@frockbot/plugin-shell 0.3.3 → 0.3.4

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-shell",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -47,41 +47,41 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@cordisjs/plugin-webui": "0.8.2",
50
- "@frockbot/agent-runtime": "0.3.3",
51
- "@frockbot/application-foundation": "0.3.3",
52
- "@frockbot/catalog-core": "0.3.3",
53
- "@frockbot/client-core": "0.3.3",
54
- "@frockbot/client-ui": "0.3.3",
55
- "@frockbot/computer-core": "0.3.3",
56
- "@frockbot/computer-host-protocol": "0.3.3",
57
- "@frockbot/configuration-core": "0.3.3",
58
- "@frockbot/connection-core": "0.3.3",
59
- "@frockbot/kernel-agent-loop": "0.3.3",
60
- "@frockbot/kernel-composition": "0.3.3",
61
- "@frockbot/kernel-contracts": "0.3.3",
62
- "@frockbot/kernel-do": "0.3.3",
63
- "@frockbot/machine-protocol": "0.3.3",
64
- "@frockbot/plugin-applets": "0.3.3",
65
- "@frockbot/plugin-authoring": "0.3.3",
66
- "@frockbot/plugin-bot-template": "0.3.3",
67
- "@frockbot/plugin-computer": "0.3.3",
68
- "@frockbot/plugin-flock": "0.3.3",
69
- "@frockbot/plugin-image": "0.3.3",
70
- "@frockbot/plugin-machine-messages": "0.3.3",
71
- "@frockbot/plugin-mcp": "0.3.3",
72
- "@frockbot/plugin-memory": "0.3.3",
73
- "@frockbot/plugin-package-catalog": "0.3.3",
74
- "@frockbot/plugin-routines": "0.3.3",
75
- "@frockbot/plugin-skills": "0.3.3",
76
- "@frockbot/plugin-subagents": "0.3.3",
77
- "@frockbot/plugin-user-machine": "0.3.3",
78
- "@frockbot/protocol": "0.3.3",
50
+ "@frockbot/agent-runtime": "0.3.4",
51
+ "@frockbot/application-foundation": "0.3.4",
52
+ "@frockbot/catalog-core": "0.3.4",
53
+ "@frockbot/client-core": "0.3.4",
54
+ "@frockbot/client-ui": "0.3.4",
55
+ "@frockbot/computer-core": "0.3.4",
56
+ "@frockbot/computer-host-protocol": "0.3.4",
57
+ "@frockbot/configuration-core": "0.3.4",
58
+ "@frockbot/connection-core": "0.3.4",
59
+ "@frockbot/kernel-agent-loop": "0.3.4",
60
+ "@frockbot/kernel-composition": "0.3.4",
61
+ "@frockbot/kernel-contracts": "0.3.4",
62
+ "@frockbot/kernel-do": "0.3.4",
63
+ "@frockbot/machine-protocol": "0.3.4",
64
+ "@frockbot/plugin-applets": "0.3.4",
65
+ "@frockbot/plugin-authoring": "0.3.4",
66
+ "@frockbot/plugin-bot-template": "0.3.4",
67
+ "@frockbot/plugin-computer": "0.3.4",
68
+ "@frockbot/plugin-flock": "0.3.4",
69
+ "@frockbot/plugin-image": "0.3.4",
70
+ "@frockbot/plugin-machine-messages": "0.3.4",
71
+ "@frockbot/plugin-mcp": "0.3.4",
72
+ "@frockbot/plugin-memory": "0.3.4",
73
+ "@frockbot/plugin-package-catalog": "0.3.4",
74
+ "@frockbot/plugin-routines": "0.3.4",
75
+ "@frockbot/plugin-skills": "0.3.4",
76
+ "@frockbot/plugin-subagents": "0.3.4",
77
+ "@frockbot/plugin-user-machine": "0.3.4",
78
+ "@frockbot/protocol": "0.3.4",
79
79
  "cordis": "4.0.0-rc.8",
80
80
  "vue": "3.5.41"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@cloudflare/workers-types": "latest",
84
- "@frockbot/plugin-tools": "0.3.3",
84
+ "@frockbot/plugin-tools": "0.3.4",
85
85
  "@types/bun": "1.3.6",
86
86
  "@types/node": "26.2.0",
87
87
  "@vitejs/plugin-vue": "6.0.8",
@@ -267,9 +267,31 @@ const macDesktop =
267
267
  typeof navigator !== "undefined" &&
268
268
  /Electron/u.test(navigator.userAgent) &&
269
269
  /Mac/u.test(navigator.platform);
270
- const botName = computed(
271
- () => state.value.botSettings?.profile.name ?? "Barebones",
272
- );
270
+ // The Bot's own name, or nothing: an account with no Bot, and a Bot whose
271
+ // settings have not arrived yet, must never be given a made-up name.
272
+ const botName = computed(() => state.value.botSettings?.profile.name ?? "");
273
+ const hasBot = computed(() => Boolean(state.value.activeBotId));
274
+ /**
275
+ * The greeting, the composer placeholder and the not-ready line all read off
276
+ * the same two facts: whether a Bot is open, and what the model resolver said.
277
+ * `state.modelLabel` already carries the resolver's own repairable failure
278
+ * sentence when the binding failed, so the surface repeats it rather than
279
+ * inventing "Model unavailable" of its own.
280
+ */
281
+ const threadHeading = computed(() => {
282
+ if (!hasBot.value) return "No Bots yet.";
283
+ if (!botName.value) return state.value.modelReady ? "Ready." : "Not ready.";
284
+ return state.value.modelReady
285
+ ? `${botName.value} is ready.`
286
+ : `${botName.value} isn't ready.`;
287
+ });
288
+ const threadHint = computed(() => {
289
+ if (!hasBot.value) return "Add your first sheep to start a conversation.";
290
+ if (state.value.modelReady) {
291
+ return "Start with a conversation. Cordis plugins can add the rest.";
292
+ }
293
+ return state.value.modelLabel;
294
+ });
273
295
  /** A Turn is executing. The composer stays open; only Stop depends on this. */
274
296
  const isRunning = computed(() => Boolean(state.value.runningRunId));
275
297
  const isConnecting = computed(() => state.value.connection !== "ready");
@@ -278,6 +300,11 @@ const isConnecting = computed(() => state.value.connection !== "ready");
278
300
  * running Turn. So the only things that close the composer are the ones that
279
301
  * would make any message impossible.
280
302
  */
303
+ const composerPlaceholder = computed(() => {
304
+ if (isConnecting.value) return "Connecting…";
305
+ if (!state.value.modelReady) return state.value.modelLabel;
306
+ return botName.value ? `Message ${botName.value}` : "Message";
307
+ });
281
308
  const canSend = computed(
282
309
  () =>
283
310
  state.value.connection === "ready" &&
@@ -778,7 +805,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
778
805
  <span class="bot-identity"
779
806
  ><k-slot name="frockbot.bot-identity"
780
807
  /></span>
781
- <div class="workspace-title">
808
+ <div v-if="hasBot" class="workspace-title">
782
809
  <strong>{{ botName }}</strong>
783
810
  <small>{{ state.modelLabel }}</small>
784
811
  </div>
@@ -793,20 +820,8 @@ function handleComposerKeydown(event: KeyboardEvent): void {
793
820
  >
794
821
  <div v-if="messages.length === 0" class="empty-thread">
795
822
  <div class="empty-mark"><UiIcon name="sparkle" size="lg" /></div>
796
- <h1>
797
- {{
798
- state.modelReady
799
- ? `${botName} is ready.`
800
- : `${botName} isn't ready.`
801
- }}
802
- </h1>
803
- <p>
804
- {{
805
- state.modelReady
806
- ? "Start with a conversation. Cordis plugins can add the rest."
807
- : "Check this Bot's model Connection."
808
- }}
809
- </p>
823
+ <h1>{{ threadHeading }}</h1>
824
+ <p>{{ threadHint }}</p>
810
825
  </div>
811
826
  <article
812
827
  v-for="message in messages"
@@ -975,7 +990,12 @@ function handleComposerKeydown(event: KeyboardEvent): void {
975
990
  </div>
976
991
  </Transition>
977
992
 
993
+ <!--
994
+ No Bot, no composer. A disabled input under a made-up Bot name reads
995
+ as a broken Bot; the first-run pane above points at making one.
996
+ -->
978
997
  <form
998
+ v-if="hasBot"
979
999
  class="composer"
980
1000
  :class="{ 'composer-busy': isRunning }"
981
1001
  @submit.prevent="sendMessage"
@@ -1038,13 +1058,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
1038
1058
  ref="composerInput"
1039
1059
  v-model="draft"
1040
1060
  aria-label="Message"
1041
- :placeholder="
1042
- isConnecting
1043
- ? 'Connecting…'
1044
- : !state.modelReady
1045
- ? 'Model unavailable'
1046
- : `Message ${botName}`
1047
- "
1061
+ :placeholder="composerPlaceholder"
1048
1062
  :disabled="isConnecting || !state.modelReady"
1049
1063
  rows="1"
1050
1064
  role="combobox"
@@ -826,6 +826,84 @@ describe("Bot selection", () => {
826
826
  },
827
827
  );
828
828
 
829
+ /**
830
+ * The composer placeholder and the not-ready line in `FrockBotApp.vue` are
831
+ * `state.modelLabel`, so a genuinely broken binding has to reach that label
832
+ * as the resolver's own repairable sentence rather than a flat "Model
833
+ * unavailable" the User cannot act on.
834
+ */
835
+ test("labels a Bot whose model Connection is disabled with the resolver's failure", async () => {
836
+ let provided: Ref<FrockBotWebData> | undefined;
837
+ const bot = initializeBotSettingsV1("broken-model-bot");
838
+ const user: UserSettingsViewV1 = {
839
+ schemaVersion: 1,
840
+ revision: 1,
841
+ profile: { name: "User" },
842
+ packages: [
843
+ { packageId: "model-provider", version: "0.0.1", state: "installed" },
844
+ ],
845
+ connections: [
846
+ {
847
+ connectionId: "model-connection",
848
+ packageId: "model-provider",
849
+ connectionTypeId: "model-account",
850
+ displayName: "Work",
851
+ state: "disabled",
852
+ providerType: "model-provider",
853
+ safeMetadata: {},
854
+ },
855
+ ],
856
+ platformModel: {
857
+ connectionId: "model-connection",
858
+ providerModelId: "model-id",
859
+ },
860
+ };
861
+ await shellClientPlugin({
862
+ transport: {
863
+ turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
864
+ readConfiguration: (query) =>
865
+ Promise.resolve(query.type === "user/get" ? user : bot),
866
+ },
867
+ slot: () => () => {},
868
+ inject: () => {
869
+ throw new Error("unexpected client provider injection");
870
+ },
871
+ provide: (_key, value) => {
872
+ provided = value as Ref<FrockBotWebData>;
873
+ return () => {};
874
+ },
875
+ });
876
+ if (!provided) throw new Error("shell data was not provided");
877
+ provided.value.activeBotId = bot.botId;
878
+ provided.value.pluginCatalog = [
879
+ {
880
+ packageId: "model-provider",
881
+ displayName: "Model provider",
882
+ version: "0.0.1",
883
+ capabilities: [
884
+ { id: "models", kind: "model", connectionTypes: ["model-account"] },
885
+ ],
886
+ connectionTypes: [
887
+ {
888
+ id: "model-account",
889
+ displayName: "Model account",
890
+ allowMultiple: false,
891
+ authorizationKind: "api-key",
892
+ capabilities: ["models"],
893
+ },
894
+ ],
895
+ settings: [],
896
+ },
897
+ ];
898
+
899
+ await provided.value.loadBotSettings();
900
+
901
+ expect(provided.value.modelReady).toBe(false);
902
+ expect(provided.value.modelLabel).toBe(
903
+ 'Connection "model-connection" is disabled; enable or reconnect it',
904
+ );
905
+ });
906
+
829
907
  test("saves Bot-scoped Package settings through the generic command", async () => {
830
908
  type PackageSettingsWebData = FrockBotWebData & {
831
909
  saveBotPackageSettings(
@@ -1088,14 +1088,18 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
1088
1088
  function updateModelLabel(): void {
1089
1089
  const bot = web.value.botSettings;
1090
1090
  const user = web.value.userSettings;
1091
- if (!bot || !user) {
1091
+ if (!user) {
1092
1092
  web.value.modelSource = "none";
1093
1093
  web.value.modelReady = false;
1094
1094
  web.value.modelLabel = modelRuntimeLabel({ source: "none" });
1095
1095
  return;
1096
1096
  }
1097
1097
  const effective = resolveEffectiveBotModelV1({
1098
- bot: toRaw(bot),
1098
+ // Before the first Bot exists — and in the window before a selected
1099
+ // Bot's settings arrive — the account's own effective model is still
1100
+ // the truth. An empty Bot scope simply declines to override it, so the
1101
+ // shell reports the account model instead of claiming it is unavailable.
1102
+ bot: bot ? toRaw(bot) : { packageValues: {} },
1099
1103
  user: toRaw(user),
1100
1104
  packages: toRaw(web.value.pluginCatalog).map((pkg) => ({
1101
1105
  packageId: pkg.packageId,
@@ -0,0 +1,122 @@
1
+ import { plugin } from "bun";
2
+ import { expect, test } from "bun:test";
3
+ import type { UserSettingsViewV1 } from "@frockbot/configuration-core";
4
+ import type { Ref } from "vue";
5
+ import type { FrockBotWebData } from "../shared.js";
6
+
7
+ // Bun has no single-file-component loader; the shell's Vue modules stand in as
8
+ // empty components, exactly as `index.test.ts` does.
9
+ plugin({
10
+ name: "shell-client-vue-no-bot-loader",
11
+ setup(build) {
12
+ build.onLoad({ filter: /\.vue$/ }, () => ({
13
+ contents: "export default {};",
14
+ loader: "js",
15
+ }));
16
+ },
17
+ });
18
+
19
+ const { shellClientPlugin } = await import("./index.js");
20
+
21
+ /**
22
+ * A first-run account: the platform model resolves against a ready ambient
23
+ * Flock AI Connection whose Catalog is fresh, and no Bot has been created yet.
24
+ * The account's model is available, so the shell must not tell the User it is
25
+ * unavailable before they have made their first Bot.
26
+ */
27
+ test("does not report the account model unavailable before a Bot exists", async () => {
28
+ const user: UserSettingsViewV1 = {
29
+ schemaVersion: 1,
30
+ revision: 3,
31
+ profile: { name: "FrockBot user" },
32
+ packages: [
33
+ { packageId: "provider-flock-ai", version: "0.0.1", state: "installed" },
34
+ ],
35
+ connections: [
36
+ {
37
+ connectionId: "flock-ai-ambient",
38
+ packageId: "provider-flock-ai",
39
+ connectionTypeId: "flock-ai-account",
40
+ displayName: "Flock AI",
41
+ state: "ready",
42
+ providerType: "flock-ai",
43
+ safeMetadata: {},
44
+ modelCatalog: {
45
+ schemaVersion: 1,
46
+ generation: "flock-ai-static-v1",
47
+ state: "fresh",
48
+ models: [
49
+ {
50
+ providerModelId: "@flock/auto",
51
+ displayName: "Auto (recommended)",
52
+ capabilities: { tools: true, vision: false, reasoning: true },
53
+ source: "discovered",
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ ],
59
+ platformModel: {
60
+ connectionId: "flock-ai-ambient",
61
+ providerModelId: "@flock/auto",
62
+ },
63
+ };
64
+
65
+ let provided: Ref<FrockBotWebData> | undefined;
66
+ await shellClientPlugin({
67
+ transport: {
68
+ turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
69
+ readApplicationManifest: () =>
70
+ Promise.resolve({
71
+ schemaVersion: 1,
72
+ deployment: { userId: "development", applicationHash: "hash-1" },
73
+ applicationHash: "hash-1",
74
+ packages: [
75
+ {
76
+ id: "provider-flock-ai",
77
+ displayName: "Flock AI",
78
+ version: "0.0.1",
79
+ contributions: ["backend", "runtime"],
80
+ configuration: {
81
+ settings: [],
82
+ connectionTypes: [
83
+ {
84
+ id: "flock-ai-account",
85
+ displayName: "Flock AI",
86
+ allowMultiple: false,
87
+ authorization: { kind: "ambient-native" },
88
+ capabilities: ["flock-ai-models"],
89
+ },
90
+ ],
91
+ capabilities: [
92
+ {
93
+ id: "flock-ai-models",
94
+ kind: "model",
95
+ connectionTypes: ["flock-ai-account"],
96
+ admission: { turnTypes: ["chat"] },
97
+ },
98
+ ],
99
+ },
100
+ },
101
+ ],
102
+ }),
103
+ readConfiguration: () => Promise.resolve(user),
104
+ },
105
+ slot: () => () => {},
106
+ inject: () => {
107
+ throw new Error("unexpected client provider injection");
108
+ },
109
+ provide: (_key, value) => {
110
+ provided = value as Ref<FrockBotWebData>;
111
+ return () => {};
112
+ },
113
+ });
114
+ if (!provided) throw new Error("shell data was not provided");
115
+
116
+ await provided.value.loadPluginCatalog();
117
+
118
+ // No Bot has been created, so `activeBotId` is unset.
119
+ expect(provided.value.activeBotId).toBeUndefined();
120
+ expect(provided.value.modelLabel).toBe("Auto (recommended) · Flock AI");
121
+ expect(provided.value.modelReady).toBe(true);
122
+ });