@frockbot/plugin-shell 0.1.3 → 0.2.0
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/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/client/index.ts
CHANGED
|
@@ -17,7 +17,12 @@ import type {
|
|
|
17
17
|
ConnectionCommandV1,
|
|
18
18
|
} from "@frockbot/connection-core";
|
|
19
19
|
import { decodeFrockBotManifest } from "@frockbot/kernel-composition";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
decodePackageIframeCatalogV1,
|
|
22
|
+
packageIframeToolAllowedV1,
|
|
23
|
+
decodeSendToUserPayloadV1,
|
|
24
|
+
type PackageIframeContributionViewV1,
|
|
25
|
+
} from "@frockbot/kernel-contracts";
|
|
21
26
|
import { createClientSurfaceRegistry } from "@frockbot/client-ui";
|
|
22
27
|
import type {
|
|
23
28
|
BotNameProvenanceV1,
|
|
@@ -25,12 +30,11 @@ import type {
|
|
|
25
30
|
BotProfile,
|
|
26
31
|
BotProfilePatchV1,
|
|
27
32
|
BotSettingsViewV1,
|
|
28
|
-
ConfigurationCommandV1,
|
|
29
33
|
JsonValue,
|
|
30
|
-
|
|
31
|
-
OperationReceiptV1,
|
|
34
|
+
PackageSettingValueV1,
|
|
32
35
|
UserSettingsViewV1,
|
|
33
36
|
} from "@frockbot/configuration-core";
|
|
37
|
+
import { resolveEffectiveBotModelV1 } from "@frockbot/configuration-core";
|
|
34
38
|
import {
|
|
35
39
|
decodeCatalogEntryV1,
|
|
36
40
|
decodeCatalogIndexV1,
|
|
@@ -49,6 +53,7 @@ import {
|
|
|
49
53
|
import { MCP_OAUTH_CONNECTION_TYPE_ID } from "@frockbot/plugin-mcp/agent";
|
|
50
54
|
import { decodeStartConnectionResultV1 } from "@frockbot/connection-core";
|
|
51
55
|
import { decodeClientSkillCatalogV1 } from "../skill-protocol.js";
|
|
56
|
+
import { decodeClientTurnV1 } from "../run-protocol.js";
|
|
52
57
|
import {
|
|
53
58
|
decodeApprovalDecisionReceiptV1,
|
|
54
59
|
decodeApprovalListViewV1,
|
|
@@ -57,7 +62,7 @@ import {
|
|
|
57
62
|
decodeTaskListViewV1,
|
|
58
63
|
decodeTaskViewV1,
|
|
59
64
|
} from "@frockbot/plugin-subagents/shared";
|
|
60
|
-
import { ref } from "vue";
|
|
65
|
+
import { ref, toRaw, type Ref } from "vue";
|
|
61
66
|
import {
|
|
62
67
|
frockBotWebDataKey,
|
|
63
68
|
type FrockBotWebData,
|
|
@@ -70,6 +75,7 @@ import {
|
|
|
70
75
|
type WebToolActivity,
|
|
71
76
|
} from "../shared.js";
|
|
72
77
|
import FrockBotApp from "./FrockBotApp.vue";
|
|
78
|
+
import PackageIframeSettings from "./PackageIframeSettings.vue";
|
|
73
79
|
import { modelRuntimeLabel } from "./model-presentation.js";
|
|
74
80
|
import { showClientNotificationV1 } from "./notify.js";
|
|
75
81
|
import "@frockbot/client-core/fonts.css";
|
|
@@ -172,7 +178,7 @@ function activeRunView(run: ClientRun): WebActiveRun | undefined {
|
|
|
172
178
|
return {
|
|
173
179
|
runId: run.runId,
|
|
174
180
|
status: run.status,
|
|
175
|
-
message: "Stop
|
|
181
|
+
message: "Stop requested; finishing up.",
|
|
176
182
|
canResume: false,
|
|
177
183
|
};
|
|
178
184
|
}
|
|
@@ -660,11 +666,21 @@ export function decodePluginCatalog(value: unknown): PluginCatalogItem[] {
|
|
|
660
666
|
connectionTypes: capability.connectionTypes,
|
|
661
667
|
}),
|
|
662
668
|
);
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
669
|
+
// User- and Bot-scoped declarations are needed for generic effective
|
|
670
|
+
// model resolution. Connection-scoped settings stay with their Connection
|
|
671
|
+
// and never enter Package-level settings forms.
|
|
672
|
+
const settings = (decoded.configuration?.settings ?? []).filter((setting) =>
|
|
673
|
+
setting.scopes.some((scope) => scope === "user" || scope === "bot"),
|
|
674
|
+
);
|
|
675
|
+
// A settings-only Package still contributes enablement: disabling it is
|
|
676
|
+
// what makes its retained controls inert. A Capability that takes no
|
|
677
|
+
// Connection likewise counts because enabling its Package grants it to
|
|
678
|
+
// all of the User's Bots.
|
|
679
|
+
if (
|
|
680
|
+
connectionTypes.length === 0 &&
|
|
681
|
+
decodedCapabilities.length === 0 &&
|
|
682
|
+
settings.length === 0
|
|
683
|
+
) {
|
|
668
684
|
return [];
|
|
669
685
|
}
|
|
670
686
|
const decodedConnections = connectionTypes.map((connection) => {
|
|
@@ -688,12 +704,7 @@ export function decodePluginCatalog(value: unknown): PluginCatalogItem[] {
|
|
|
688
704
|
version: candidate.version,
|
|
689
705
|
capabilities: decodedCapabilities,
|
|
690
706
|
connectionTypes: decodedConnections,
|
|
691
|
-
|
|
692
|
-
// surface's to edit, and a Connection-scoped one is edited with its
|
|
693
|
-
// Connection.
|
|
694
|
-
settings: (decoded.configuration?.settings ?? []).filter((setting) =>
|
|
695
|
-
setting.scopes.includes("user"),
|
|
696
|
-
),
|
|
707
|
+
settings,
|
|
697
708
|
},
|
|
698
709
|
];
|
|
699
710
|
});
|
|
@@ -977,31 +988,6 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
977
988
|
}
|
|
978
989
|
}
|
|
979
990
|
|
|
980
|
-
async function executeAssignmentOperation(
|
|
981
|
-
command: Extract<
|
|
982
|
-
ConfigurationCommandV1,
|
|
983
|
-
{
|
|
984
|
-
type:
|
|
985
|
-
| "bot/assign-capability"
|
|
986
|
-
| "bot/replace-capability"
|
|
987
|
-
| "bot/unassign-capability";
|
|
988
|
-
}
|
|
989
|
-
>,
|
|
990
|
-
): Promise<void> {
|
|
991
|
-
const execute = ctx.transport.executeConfiguration;
|
|
992
|
-
if (!execute) throw new Error("Settings are unavailable");
|
|
993
|
-
const receipt = (await execute(command)) as OperationReceiptV1;
|
|
994
|
-
await web.value.loadBotSettings();
|
|
995
|
-
if (receipt.status === "rejected") {
|
|
996
|
-
const failure = receipt.failure ?? "Assignment operation was rejected";
|
|
997
|
-
web.value.settingsError = failure;
|
|
998
|
-
throw new Error(failure);
|
|
999
|
-
}
|
|
1000
|
-
if (receipt.status === "pending") {
|
|
1001
|
-
web.value.settingsError = "Assignment operation is retrying.";
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
991
|
function updateSettingsLoadError(
|
|
1006
992
|
source: "bot" | "user" | "catalog" | "package-catalog",
|
|
1007
993
|
message?: string,
|
|
@@ -1011,63 +997,55 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1011
997
|
web.value.settingsError = [...settingsLoadErrors.values()].at(-1);
|
|
1012
998
|
}
|
|
1013
999
|
|
|
1014
|
-
/**
|
|
1015
|
-
* The Bot runs on its own model when it has one and on the User's default
|
|
1016
|
-
* otherwise, so readiness and the composer label follow the effective model.
|
|
1017
|
-
* A Bot following the default is ready as soon as the User's Connection is:
|
|
1018
|
-
* the Bot's own Assignment for that Connection is claimed durably when the
|
|
1019
|
-
* Turn is admitted.
|
|
1020
|
-
*/
|
|
1000
|
+
/** Readiness and the composer label follow the generic effective model. */
|
|
1021
1001
|
function updateModelLabel(): void {
|
|
1022
1002
|
const bot = web.value.botSettings;
|
|
1023
1003
|
const user = web.value.userSettings;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
(
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1004
|
+
if (!bot || !user) {
|
|
1005
|
+
web.value.modelSource = "none";
|
|
1006
|
+
web.value.modelReady = false;
|
|
1007
|
+
web.value.modelLabel = modelRuntimeLabel({ source: "none" });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const effective = resolveEffectiveBotModelV1({
|
|
1011
|
+
bot: toRaw(bot),
|
|
1012
|
+
user: toRaw(user),
|
|
1013
|
+
packages: toRaw(web.value.pluginCatalog).map((pkg) => ({
|
|
1014
|
+
packageId: pkg.packageId,
|
|
1015
|
+
version: pkg.version,
|
|
1016
|
+
settings: pkg.settings ?? [],
|
|
1017
|
+
capabilities: pkg.capabilities,
|
|
1018
|
+
connectionTypes: pkg.connectionTypes,
|
|
1019
|
+
})),
|
|
1020
|
+
});
|
|
1021
|
+
// `FrockBotWebData`'s source vocabulary is owned outside this lane. Until
|
|
1022
|
+
// it adopts the core's four sources, both inherited choices are its
|
|
1023
|
+
// existing `default` projection; the label still preserves the exact core
|
|
1024
|
+
// source below.
|
|
1025
|
+
web.value.modelSource =
|
|
1026
|
+
effective.source === "bot"
|
|
1027
|
+
? "bot"
|
|
1028
|
+
: effective.source === "none"
|
|
1029
|
+
? "none"
|
|
1030
|
+
: "default";
|
|
1031
|
+
web.value.modelReady = Boolean(
|
|
1032
|
+
effective.binding && effective.binding.state !== "unavailable",
|
|
1032
1033
|
);
|
|
1034
|
+
const connection = effective.binding?.connection;
|
|
1033
1035
|
const catalogPackage = web.value.pluginCatalog.find(
|
|
1034
|
-
(pkg) => pkg.packageId ===
|
|
1035
|
-
);
|
|
1036
|
-
const connectionType = catalogPackage?.connectionTypes.find(
|
|
1037
|
-
(candidate) => candidate.id === connection?.connectionTypeId,
|
|
1038
|
-
);
|
|
1039
|
-
const modelCapabilities = new Set(
|
|
1040
|
-
catalogPackage?.capabilities.flatMap((capability) =>
|
|
1041
|
-
capability.kind === "model" &&
|
|
1042
|
-
connectionType?.capabilities.includes(capability.id)
|
|
1043
|
-
? [capability.id]
|
|
1044
|
-
: [],
|
|
1045
|
-
) ?? [],
|
|
1046
|
-
);
|
|
1047
|
-
const authorized =
|
|
1048
|
-
web.value.modelSource === "bot"
|
|
1049
|
-
? Boolean(
|
|
1050
|
-
bot?.assignments.some(
|
|
1051
|
-
(assignment) =>
|
|
1052
|
-
assignment.connectionId === model?.connectionId &&
|
|
1053
|
-
assignment.packageId === connection?.packageId &&
|
|
1054
|
-
assignment.state === "enabled" &&
|
|
1055
|
-
modelCapabilities.has(assignment.capabilityId),
|
|
1056
|
-
),
|
|
1057
|
-
)
|
|
1058
|
-
: modelCapabilities.size > 0;
|
|
1059
|
-
web.value.modelReady = Boolean(
|
|
1060
|
-
model && connection?.state === "ready" && packageInstalled && authorized,
|
|
1036
|
+
(pkg) => pkg.packageId === effective.binding?.packageId,
|
|
1061
1037
|
);
|
|
1062
1038
|
const catalogModel = connection?.modelCatalog?.models.find(
|
|
1063
|
-
(candidate) =>
|
|
1039
|
+
(candidate) =>
|
|
1040
|
+
candidate.providerModelId === effective.model?.providerModelId,
|
|
1064
1041
|
);
|
|
1065
1042
|
web.value.modelLabel = modelRuntimeLabel({
|
|
1043
|
+
source: effective.source,
|
|
1066
1044
|
modelDisplayName: catalogModel?.displayName,
|
|
1067
|
-
providerModelId: model?.providerModelId,
|
|
1045
|
+
providerModelId: effective.model?.providerModelId,
|
|
1068
1046
|
packageDisplayName: catalogPackage?.displayName,
|
|
1069
1047
|
connectionDisplayName: connection?.displayName,
|
|
1070
|
-
|
|
1048
|
+
failure: effective.binding?.failure,
|
|
1071
1049
|
});
|
|
1072
1050
|
}
|
|
1073
1051
|
|
|
@@ -1102,9 +1080,16 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1102
1080
|
await web.value.loadMcpServers();
|
|
1103
1081
|
}
|
|
1104
1082
|
|
|
1105
|
-
|
|
1083
|
+
type ShellWebData = FrockBotWebData & {
|
|
1084
|
+
saveBotPackageSettings(
|
|
1085
|
+
packageId: string,
|
|
1086
|
+
values: Record<string, PackageSettingValueV1>,
|
|
1087
|
+
): Promise<void>;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const web: Ref<ShellWebData> = ref({
|
|
1106
1091
|
connection: "ready",
|
|
1107
|
-
modelLabel: "
|
|
1092
|
+
modelLabel: "Model unavailable",
|
|
1108
1093
|
modelReady: false,
|
|
1109
1094
|
modelSource: "none",
|
|
1110
1095
|
settingsAvailable: true,
|
|
@@ -1117,6 +1102,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1117
1102
|
skillCatalog: [],
|
|
1118
1103
|
approvals: [],
|
|
1119
1104
|
tasks: [],
|
|
1105
|
+
packageUi: undefined,
|
|
1120
1106
|
async selectBot(botId: string): Promise<void> {
|
|
1121
1107
|
// Re-selecting the open Bot is not a switch: aborting the live Turn and
|
|
1122
1108
|
// clearing the transcript would discard state the User is watching.
|
|
@@ -1135,6 +1121,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1135
1121
|
web.value.skillCatalog = [];
|
|
1136
1122
|
web.value.approvals = [];
|
|
1137
1123
|
web.value.tasks = [];
|
|
1124
|
+
web.value.packageUi = undefined;
|
|
1138
1125
|
const url = URL.parse(window.location.href);
|
|
1139
1126
|
if (url) {
|
|
1140
1127
|
url.searchParams.set("bot", botId);
|
|
@@ -1218,6 +1205,68 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1218
1205
|
web.value.tasks = [];
|
|
1219
1206
|
}
|
|
1220
1207
|
},
|
|
1208
|
+
async loadPackageUi(): Promise<void> {
|
|
1209
|
+
const read = ctx.transport.hostedRequest;
|
|
1210
|
+
const botId = web.value.activeBotId;
|
|
1211
|
+
if (!read || !botId) return;
|
|
1212
|
+
const generation = selectionGeneration;
|
|
1213
|
+
try {
|
|
1214
|
+
const catalog = decodePackageIframeCatalogV1(
|
|
1215
|
+
await read(`/api/bots/${encodeURIComponent(botId)}/package-ui`),
|
|
1216
|
+
);
|
|
1217
|
+
if (
|
|
1218
|
+
generation !== selectionGeneration ||
|
|
1219
|
+
web.value.activeBotId !== botId
|
|
1220
|
+
)
|
|
1221
|
+
return;
|
|
1222
|
+
web.value.packageUi = catalog;
|
|
1223
|
+
} catch {
|
|
1224
|
+
if (
|
|
1225
|
+
generation === selectionGeneration &&
|
|
1226
|
+
web.value.activeBotId === botId
|
|
1227
|
+
) {
|
|
1228
|
+
web.value.packageUi = undefined;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
async callPackageUiTool(
|
|
1233
|
+
contribution: PackageIframeContributionViewV1,
|
|
1234
|
+
name: string,
|
|
1235
|
+
input: unknown,
|
|
1236
|
+
): Promise<unknown> {
|
|
1237
|
+
const post = ctx.transport.hostedRequest;
|
|
1238
|
+
const botId = web.value.activeBotId;
|
|
1239
|
+
const catalog = web.value.packageUi;
|
|
1240
|
+
if (!post || !botId || !catalog || catalog.botId !== botId) {
|
|
1241
|
+
throw new Error("Package UI is unavailable");
|
|
1242
|
+
}
|
|
1243
|
+
if (!packageIframeToolAllowedV1(contribution, name)) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`Package "${contribution.packageId}" did not declare tool "${name}"`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const turn = decodeClientTurnV1(
|
|
1249
|
+
await post(
|
|
1250
|
+
`/api/bots/${encodeURIComponent(botId)}/package-ui/tools`,
|
|
1251
|
+
"POST",
|
|
1252
|
+
JSON.stringify({
|
|
1253
|
+
schemaVersion: 1,
|
|
1254
|
+
commandId: crypto.randomUUID(),
|
|
1255
|
+
generationId: catalog.generationId,
|
|
1256
|
+
packageId: contribution.packageId,
|
|
1257
|
+
name,
|
|
1258
|
+
input,
|
|
1259
|
+
}),
|
|
1260
|
+
),
|
|
1261
|
+
);
|
|
1262
|
+
await deliverNotifications(botId);
|
|
1263
|
+
const result = turn.events.findLast(
|
|
1264
|
+
(event) => event.type === "tool/result",
|
|
1265
|
+
);
|
|
1266
|
+
return result?.type === "tool/result"
|
|
1267
|
+
? { content: result.content, isError: result.isError === true }
|
|
1268
|
+
: { content: turn.text, isError: false };
|
|
1269
|
+
},
|
|
1221
1270
|
/**
|
|
1222
1271
|
* Explicit, authenticated cancellation of one subagent from the client.
|
|
1223
1272
|
*
|
|
@@ -1298,8 +1347,8 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1298
1347
|
)
|
|
1299
1348
|
return;
|
|
1300
1349
|
web.value.botSettings = settings;
|
|
1301
|
-
// The effective model may
|
|
1302
|
-
// needs the User settings too. Loading them never fails this read:
|
|
1350
|
+
// The effective model may come from account or platform state, so Bot
|
|
1351
|
+
// readiness needs the User settings too. Loading them never fails this read:
|
|
1303
1352
|
// `loadUserSettings` reports its own failure.
|
|
1304
1353
|
if (!web.value.userSettings) await web.value.loadUserSettings();
|
|
1305
1354
|
if (
|
|
@@ -1310,6 +1359,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1310
1359
|
updateModelLabel();
|
|
1311
1360
|
updateSettingsLoadError("bot");
|
|
1312
1361
|
await deliverNotifications(botId, generation);
|
|
1362
|
+
await web.value.loadPackageUi();
|
|
1313
1363
|
} catch (error) {
|
|
1314
1364
|
if (
|
|
1315
1365
|
generation !== selectionGeneration ||
|
|
@@ -1389,153 +1439,23 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1389
1439
|
});
|
|
1390
1440
|
await web.value.loadBotSettings();
|
|
1391
1441
|
},
|
|
1392
|
-
async
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
throw new Error("Settings are unavailable");
|
|
1397
|
-
}
|
|
1398
|
-
await executeAssignmentOperation({
|
|
1399
|
-
schemaVersion: 1,
|
|
1400
|
-
type: "bot/assign-capability",
|
|
1401
|
-
commandId: crypto.randomUUID(),
|
|
1402
|
-
botId,
|
|
1403
|
-
expectedRevision: current.revision,
|
|
1404
|
-
assignment,
|
|
1405
|
-
});
|
|
1406
|
-
},
|
|
1407
|
-
async replaceCapability(assignment): Promise<void> {
|
|
1408
|
-
const current = web.value.botSettings;
|
|
1409
|
-
const botId = web.value.activeBotId;
|
|
1410
|
-
if (!current || !botId || !ctx.transport.executeConfiguration) {
|
|
1411
|
-
throw new Error("Settings are unavailable");
|
|
1412
|
-
}
|
|
1413
|
-
await executeAssignmentOperation({
|
|
1414
|
-
schemaVersion: 1,
|
|
1415
|
-
type: "bot/replace-capability",
|
|
1416
|
-
commandId: crypto.randomUUID(),
|
|
1417
|
-
botId,
|
|
1418
|
-
expectedRevision: current.revision,
|
|
1419
|
-
assignment,
|
|
1420
|
-
});
|
|
1421
|
-
},
|
|
1422
|
-
async unassignCapability(assignmentId): Promise<void> {
|
|
1442
|
+
async saveBotPackageSettings(
|
|
1443
|
+
packageId: string,
|
|
1444
|
+
values: Record<string, PackageSettingValueV1>,
|
|
1445
|
+
): Promise<void> {
|
|
1423
1446
|
const current = web.value.botSettings;
|
|
1424
1447
|
const botId = web.value.activeBotId;
|
|
1425
1448
|
if (!current || !botId || !ctx.transport.executeConfiguration) {
|
|
1426
1449
|
throw new Error("Settings are unavailable");
|
|
1427
1450
|
}
|
|
1428
|
-
await executeAssignmentOperation({
|
|
1429
|
-
schemaVersion: 1,
|
|
1430
|
-
type: "bot/unassign-capability",
|
|
1431
|
-
commandId: crypto.randomUUID(),
|
|
1432
|
-
botId,
|
|
1433
|
-
expectedRevision: current.revision,
|
|
1434
|
-
assignmentId,
|
|
1435
|
-
});
|
|
1436
|
-
},
|
|
1437
|
-
async saveBotModel(model: ModelAssignment): Promise<void> {
|
|
1438
|
-
const current = web.value.botSettings;
|
|
1439
|
-
const user = web.value.userSettings;
|
|
1440
|
-
const botId = web.value.activeBotId;
|
|
1441
|
-
if (!current || !user || !botId || !ctx.transport.executeConfiguration) {
|
|
1442
|
-
throw new Error("Settings are unavailable");
|
|
1443
|
-
}
|
|
1444
|
-
const modelChanged =
|
|
1445
|
-
current.model?.connectionId !== model.connectionId ||
|
|
1446
|
-
current.model?.providerModelId !== model.providerModelId;
|
|
1447
|
-
const connection = user.connections.find(
|
|
1448
|
-
(candidate) => candidate.connectionId === model.connectionId,
|
|
1449
|
-
);
|
|
1450
|
-
if (!modelChanged && connection?.state !== "ready") return;
|
|
1451
|
-
const pkg = web.value.pluginCatalog.find(
|
|
1452
|
-
(candidate) => candidate.packageId === connection?.packageId,
|
|
1453
|
-
);
|
|
1454
|
-
const connectionType = pkg?.connectionTypes.find(
|
|
1455
|
-
(candidate) => candidate.id === connection?.connectionTypeId,
|
|
1456
|
-
);
|
|
1457
|
-
const capability = pkg?.capabilities.find(
|
|
1458
|
-
(candidate) =>
|
|
1459
|
-
candidate.kind === "model" &&
|
|
1460
|
-
connectionType?.capabilities.includes(candidate.id),
|
|
1461
|
-
);
|
|
1462
|
-
if (!connection || connection.state !== "ready" || !pkg || !capability) {
|
|
1463
|
-
throw new Error("The selected Connection has no model capability");
|
|
1464
|
-
}
|
|
1465
|
-
const assigned = current.assignments.some(
|
|
1466
|
-
(assignment) =>
|
|
1467
|
-
assignment.state === "enabled" &&
|
|
1468
|
-
assignment.packageId === pkg.packageId &&
|
|
1469
|
-
assignment.capabilityId === capability.id &&
|
|
1470
|
-
assignment.connectionId === connection.connectionId,
|
|
1471
|
-
);
|
|
1472
|
-
if (assigned && !modelChanged) return;
|
|
1473
|
-
if (!assigned) {
|
|
1474
|
-
// The binding commits inside the Assignment saga's commit phase, so
|
|
1475
|
-
// the Connection claim and the Bot's model are one durable unit. An
|
|
1476
|
-
// existing model Assignment on another Connection is replaced
|
|
1477
|
-
// atomically rather than unassigned and assigned again.
|
|
1478
|
-
const superseded = current.assignments.find(
|
|
1479
|
-
(assignment) =>
|
|
1480
|
-
assignment.packageId === pkg.packageId &&
|
|
1481
|
-
assignment.capabilityId === capability.id,
|
|
1482
|
-
);
|
|
1483
|
-
await executeAssignmentOperation({
|
|
1484
|
-
schemaVersion: 1,
|
|
1485
|
-
type: superseded ? "bot/replace-capability" : "bot/assign-capability",
|
|
1486
|
-
commandId: crypto.randomUUID(),
|
|
1487
|
-
botId,
|
|
1488
|
-
expectedRevision: current.revision,
|
|
1489
|
-
assignment: {
|
|
1490
|
-
assignmentId: superseded?.assignmentId ?? crypto.randomUUID(),
|
|
1491
|
-
packageId: pkg.packageId,
|
|
1492
|
-
capabilityId: capability.id,
|
|
1493
|
-
connectionId: connection.connectionId,
|
|
1494
|
-
},
|
|
1495
|
-
model,
|
|
1496
|
-
});
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
1451
|
const receipt = await ctx.transport.executeConfiguration({
|
|
1500
1452
|
schemaVersion: 1,
|
|
1501
|
-
type: "bot/
|
|
1453
|
+
type: "bot/set-package-settings",
|
|
1502
1454
|
commandId: crypto.randomUUID(),
|
|
1503
1455
|
botId,
|
|
1504
1456
|
expectedRevision: current.revision,
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
await web.value.loadBotSettings();
|
|
1508
|
-
if (receipt.status === "rejected") {
|
|
1509
|
-
throw new Error(receipt.failure);
|
|
1510
|
-
}
|
|
1511
|
-
},
|
|
1512
|
-
async clearBotModel(): Promise<void> {
|
|
1513
|
-
const current = web.value.botSettings;
|
|
1514
|
-
const botId = web.value.activeBotId;
|
|
1515
|
-
if (!current?.model || !botId || !ctx.transport.executeConfiguration) {
|
|
1516
|
-
throw new Error("Settings are unavailable");
|
|
1517
|
-
}
|
|
1518
|
-
const assignment = current.assignments.find((candidate) => {
|
|
1519
|
-
const capability = web.value.pluginCatalog
|
|
1520
|
-
.find((pkg) => pkg.packageId === candidate.packageId)
|
|
1521
|
-
?.capabilities.find(
|
|
1522
|
-
(declared) => declared.id === candidate.capabilityId,
|
|
1523
|
-
);
|
|
1524
|
-
return (
|
|
1525
|
-
(candidate.state === "enabled" ||
|
|
1526
|
-
candidate.state === "unavailable") &&
|
|
1527
|
-
candidate.connectionId === current.model?.connectionId &&
|
|
1528
|
-
capability?.kind === "model"
|
|
1529
|
-
);
|
|
1530
|
-
});
|
|
1531
|
-
if (!assignment) throw new Error("Bot model assignment is unavailable");
|
|
1532
|
-
const receipt = await ctx.transport.executeConfiguration({
|
|
1533
|
-
schemaVersion: 1,
|
|
1534
|
-
type: "bot/unbind-model",
|
|
1535
|
-
commandId: crypto.randomUUID(),
|
|
1536
|
-
botId,
|
|
1537
|
-
expectedRevision: current.revision,
|
|
1538
|
-
assignmentId: assignment.assignmentId,
|
|
1457
|
+
packageId,
|
|
1458
|
+
values,
|
|
1539
1459
|
});
|
|
1540
1460
|
await web.value.loadBotSettings();
|
|
1541
1461
|
if (receipt.status === "rejected") throw new Error(receipt.failure);
|
|
@@ -1585,30 +1505,6 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1585
1505
|
});
|
|
1586
1506
|
await web.value.loadUserSettings();
|
|
1587
1507
|
},
|
|
1588
|
-
async saveDefaultModel(model: ModelAssignment | undefined): Promise<void> {
|
|
1589
|
-
const settings = web.value.userSettings;
|
|
1590
|
-
if (!settings || !ctx.transport.executeConfiguration) {
|
|
1591
|
-
throw new Error("Settings are unavailable");
|
|
1592
|
-
}
|
|
1593
|
-
const current = settings.newBotModelTemplate;
|
|
1594
|
-
if (
|
|
1595
|
-
current?.connectionId === model?.connectionId &&
|
|
1596
|
-
current?.providerModelId === model?.providerModelId &&
|
|
1597
|
-
settings.newBotModelTemplateSource === "user"
|
|
1598
|
-
) {
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
const receipt = await ctx.transport.executeConfiguration({
|
|
1602
|
-
schemaVersion: 1,
|
|
1603
|
-
type: "user/set-new-bot-model",
|
|
1604
|
-
commandId: crypto.randomUUID(),
|
|
1605
|
-
expectedRevision: settings.revision,
|
|
1606
|
-
...(model ? { model } : {}),
|
|
1607
|
-
source: "user",
|
|
1608
|
-
});
|
|
1609
|
-
await web.value.loadUserSettings();
|
|
1610
|
-
if (receipt.status === "rejected") throw new Error(receipt.failure);
|
|
1611
|
-
},
|
|
1612
1508
|
async loadPluginCatalog(): Promise<void> {
|
|
1613
1509
|
if (
|
|
1614
1510
|
!ctx.transport.readApplicationManifest ||
|
|
@@ -1846,7 +1742,10 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1846
1742
|
// Enablement is projected onto the installation row the Plugins surface
|
|
1847
1743
|
// renders, so the toggle reads back what the User authority recorded.
|
|
1848
1744
|
await web.value.loadPluginCatalog();
|
|
1849
|
-
if (receipt.status === "rejected")
|
|
1745
|
+
if (receipt.status === "rejected") {
|
|
1746
|
+
web.value.settingsError = receipt.failure;
|
|
1747
|
+
throw new Error(receipt.failure);
|
|
1748
|
+
}
|
|
1850
1749
|
},
|
|
1851
1750
|
async savePackageSettings(
|
|
1852
1751
|
packageId: string,
|
|
@@ -2187,7 +2086,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2187
2086
|
runId: pendingRunId,
|
|
2188
2087
|
role: "assistant",
|
|
2189
2088
|
text: aborted
|
|
2190
|
-
? "Request stopped locally;
|
|
2089
|
+
? "Request stopped locally; checking whether it started."
|
|
2191
2090
|
: "Confirming whether this Turn was admitted.",
|
|
2192
2091
|
at: optimisticAt,
|
|
2193
2092
|
status: "interrupted",
|
|
@@ -2246,7 +2145,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2246
2145
|
web.value.activeRun = {
|
|
2247
2146
|
runId,
|
|
2248
2147
|
status: "running",
|
|
2249
|
-
message: "Reconciliation requested;
|
|
2148
|
+
message: "Reconciliation requested; checking progress.",
|
|
2250
2149
|
canResume: false,
|
|
2251
2150
|
};
|
|
2252
2151
|
const botId = web.value.activeBotId;
|
|
@@ -2314,16 +2213,23 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2314
2213
|
async abort() {
|
|
2315
2214
|
activeRequest?.abort();
|
|
2316
2215
|
},
|
|
2317
|
-
})
|
|
2216
|
+
} satisfies Partial<ShellWebData>) as unknown as Ref<ShellWebData>;
|
|
2318
2217
|
|
|
2319
2218
|
return [
|
|
2320
2219
|
ctx.provide(clientSurfaceRegistryKey, surfaces),
|
|
2321
|
-
|
|
2220
|
+
// The shared client projection is updated by the contracts lane. This cast
|
|
2221
|
+
// is the seam between its retired methods and this lane's replacement.
|
|
2222
|
+
ctx.provide(frockBotWebDataKey, web as unknown as Ref<FrockBotWebData>),
|
|
2322
2223
|
ctx.slot({
|
|
2323
2224
|
slot: "authenticated-root",
|
|
2324
2225
|
order: 10_000,
|
|
2325
2226
|
component: FrockBotApp,
|
|
2326
2227
|
}),
|
|
2228
|
+
ctx.slot({
|
|
2229
|
+
slot: "frockbot.bot-settings-sections",
|
|
2230
|
+
order: 10_000,
|
|
2231
|
+
component: PackageIframeSettings,
|
|
2232
|
+
}),
|
|
2327
2233
|
() => {
|
|
2328
2234
|
activeRequest?.abort();
|
|
2329
2235
|
admissionObserver?.abort();
|
|
@@ -2,34 +2,47 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { modelRuntimeLabel } from "./model-presentation.js";
|
|
3
3
|
|
|
4
4
|
describe("model runtime presentation", () => {
|
|
5
|
-
test("names
|
|
5
|
+
test("names platform models plainly", () => {
|
|
6
6
|
expect(
|
|
7
7
|
modelRuntimeLabel({
|
|
8
|
+
source: "platform",
|
|
8
9
|
modelDisplayName: "Llama 3",
|
|
9
10
|
providerModelId: "llama-3:cloud",
|
|
10
11
|
packageDisplayName: "Ollama Cloud",
|
|
11
12
|
connectionDisplayName: "Work",
|
|
12
|
-
hasModel: true,
|
|
13
13
|
}),
|
|
14
14
|
).toBe("Llama 3 · Ollama Cloud");
|
|
15
15
|
expect(
|
|
16
16
|
modelRuntimeLabel({
|
|
17
|
+
source: "platform",
|
|
17
18
|
providerModelId: "llama-3:cloud",
|
|
18
19
|
connectionDisplayName: "Custom provider",
|
|
19
|
-
hasModel: true,
|
|
20
20
|
}),
|
|
21
21
|
).toBe("llama-3:cloud · Custom provider");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
test("
|
|
24
|
+
test("distinguishes opt-in account and Bot choices", () => {
|
|
25
25
|
const label = {
|
|
26
26
|
modelDisplayName: "Llama 3",
|
|
27
|
+
providerModelId: "llama-3:cloud",
|
|
27
28
|
packageDisplayName: "Ollama Cloud",
|
|
28
|
-
hasModel: true,
|
|
29
29
|
};
|
|
30
|
-
expect(modelRuntimeLabel(label)).toBe(
|
|
31
|
-
|
|
32
|
-
"No default model",
|
|
30
|
+
expect(modelRuntimeLabel({ ...label, source: "account" })).toBe(
|
|
31
|
+
"Llama 3 · Ollama Cloud · Account model",
|
|
33
32
|
);
|
|
33
|
+
expect(modelRuntimeLabel({ ...label, source: "bot" })).toBe(
|
|
34
|
+
"Llama 3 · Ollama Cloud · Bot override",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("shows unavailable and backend failure states", () => {
|
|
39
|
+
expect(modelRuntimeLabel({ source: "none" })).toBe("Model unavailable");
|
|
40
|
+
expect(
|
|
41
|
+
modelRuntimeLabel({
|
|
42
|
+
source: "account",
|
|
43
|
+
providerModelId: "llama-3:cloud",
|
|
44
|
+
failure: 'Connection "work" is revoked; enable or reconnect it',
|
|
45
|
+
}),
|
|
46
|
+
).toBe('Connection "work" is revoked; enable or reconnect it');
|
|
34
47
|
});
|
|
35
48
|
});
|