@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 +31 -31
- package/src/client/FrockBotApp.vue +39 -25
- package/src/client/index.test.ts +78 -0
- package/src/client/index.ts +6 -2
- package/src/client/no-bot-model-label.test.ts +122 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.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.
|
|
51
|
-
"@frockbot/application-foundation": "0.3.
|
|
52
|
-
"@frockbot/catalog-core": "0.3.
|
|
53
|
-
"@frockbot/client-core": "0.3.
|
|
54
|
-
"@frockbot/client-ui": "0.3.
|
|
55
|
-
"@frockbot/computer-core": "0.3.
|
|
56
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
57
|
-
"@frockbot/configuration-core": "0.3.
|
|
58
|
-
"@frockbot/connection-core": "0.3.
|
|
59
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
60
|
-
"@frockbot/kernel-composition": "0.3.
|
|
61
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
62
|
-
"@frockbot/kernel-do": "0.3.
|
|
63
|
-
"@frockbot/machine-protocol": "0.3.
|
|
64
|
-
"@frockbot/plugin-applets": "0.3.
|
|
65
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
66
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
67
|
-
"@frockbot/plugin-computer": "0.3.
|
|
68
|
-
"@frockbot/plugin-flock": "0.3.
|
|
69
|
-
"@frockbot/plugin-image": "0.3.
|
|
70
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
71
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
72
|
-
"@frockbot/plugin-memory": "0.3.
|
|
73
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
74
|
-
"@frockbot/plugin-routines": "0.3.
|
|
75
|
-
"@frockbot/plugin-skills": "0.3.
|
|
76
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
77
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
78
|
-
"@frockbot/protocol": "0.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.
|
|
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
|
-
|
|
271
|
-
|
|
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"
|
package/src/client/index.test.ts
CHANGED
|
@@ -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(
|
package/src/client/index.ts
CHANGED
|
@@ -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 (!
|
|
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
|
-
|
|
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
|
+
});
|