@frockbot/plugin-shell 0.1.2 → 0.1.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 +28 -28
- package/src/backend-composition.ts +4 -4
- package/src/backend-configuration.test.ts +366 -1451
- package/src/backend-execution.ts +0 -4
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +181 -35
- package/src/backend-isolate.ts +110 -72
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend.ts +335 -1659
- package/src/client/FrockBotApp.vue +6 -73
- package/src/client/index.test.ts +320 -380
- package/src/client/index.ts +85 -256
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/styles.css +0 -27
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +0 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/client/index.ts
CHANGED
|
@@ -25,12 +25,11 @@ import type {
|
|
|
25
25
|
BotProfile,
|
|
26
26
|
BotProfilePatchV1,
|
|
27
27
|
BotSettingsViewV1,
|
|
28
|
-
ConfigurationCommandV1,
|
|
29
28
|
JsonValue,
|
|
30
|
-
|
|
31
|
-
OperationReceiptV1,
|
|
29
|
+
PackageSettingValueV1,
|
|
32
30
|
UserSettingsViewV1,
|
|
33
31
|
} from "@frockbot/configuration-core";
|
|
32
|
+
import { resolveEffectiveBotModelV1 } from "@frockbot/configuration-core";
|
|
34
33
|
import {
|
|
35
34
|
decodeCatalogEntryV1,
|
|
36
35
|
decodeCatalogIndexV1,
|
|
@@ -57,7 +56,7 @@ import {
|
|
|
57
56
|
decodeTaskListViewV1,
|
|
58
57
|
decodeTaskViewV1,
|
|
59
58
|
} from "@frockbot/plugin-subagents/shared";
|
|
60
|
-
import { ref } from "vue";
|
|
59
|
+
import { ref, toRaw, type Ref } from "vue";
|
|
61
60
|
import {
|
|
62
61
|
frockBotWebDataKey,
|
|
63
62
|
type FrockBotWebData,
|
|
@@ -172,7 +171,7 @@ function activeRunView(run: ClientRun): WebActiveRun | undefined {
|
|
|
172
171
|
return {
|
|
173
172
|
runId: run.runId,
|
|
174
173
|
status: run.status,
|
|
175
|
-
message: "Stop
|
|
174
|
+
message: "Stop requested; finishing up.",
|
|
176
175
|
canResume: false,
|
|
177
176
|
};
|
|
178
177
|
}
|
|
@@ -660,11 +659,21 @@ export function decodePluginCatalog(value: unknown): PluginCatalogItem[] {
|
|
|
660
659
|
connectionTypes: capability.connectionTypes,
|
|
661
660
|
}),
|
|
662
661
|
);
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
662
|
+
// User- and Bot-scoped declarations are needed for generic effective
|
|
663
|
+
// model resolution. Connection-scoped settings stay with their Connection
|
|
664
|
+
// and never enter Package-level settings forms.
|
|
665
|
+
const settings = (decoded.configuration?.settings ?? []).filter((setting) =>
|
|
666
|
+
setting.scopes.some((scope) => scope === "user" || scope === "bot"),
|
|
667
|
+
);
|
|
668
|
+
// A settings-only Package still contributes enablement: disabling it is
|
|
669
|
+
// what makes its retained controls inert. A Capability that takes no
|
|
670
|
+
// Connection likewise counts because enabling its Package grants it to
|
|
671
|
+
// all of the User's Bots.
|
|
672
|
+
if (
|
|
673
|
+
connectionTypes.length === 0 &&
|
|
674
|
+
decodedCapabilities.length === 0 &&
|
|
675
|
+
settings.length === 0
|
|
676
|
+
) {
|
|
668
677
|
return [];
|
|
669
678
|
}
|
|
670
679
|
const decodedConnections = connectionTypes.map((connection) => {
|
|
@@ -688,12 +697,7 @@ export function decodePluginCatalog(value: unknown): PluginCatalogItem[] {
|
|
|
688
697
|
version: candidate.version,
|
|
689
698
|
capabilities: decodedCapabilities,
|
|
690
699
|
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
|
-
),
|
|
700
|
+
settings,
|
|
697
701
|
},
|
|
698
702
|
];
|
|
699
703
|
});
|
|
@@ -977,31 +981,6 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
977
981
|
}
|
|
978
982
|
}
|
|
979
983
|
|
|
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
984
|
function updateSettingsLoadError(
|
|
1006
985
|
source: "bot" | "user" | "catalog" | "package-catalog",
|
|
1007
986
|
message?: string,
|
|
@@ -1011,63 +990,55 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1011
990
|
web.value.settingsError = [...settingsLoadErrors.values()].at(-1);
|
|
1012
991
|
}
|
|
1013
992
|
|
|
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
|
-
*/
|
|
993
|
+
/** Readiness and the composer label follow the generic effective model. */
|
|
1021
994
|
function updateModelLabel(): void {
|
|
1022
995
|
const bot = web.value.botSettings;
|
|
1023
996
|
const user = web.value.userSettings;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
(
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
997
|
+
if (!bot || !user) {
|
|
998
|
+
web.value.modelSource = "none";
|
|
999
|
+
web.value.modelReady = false;
|
|
1000
|
+
web.value.modelLabel = modelRuntimeLabel({ source: "none" });
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const effective = resolveEffectiveBotModelV1({
|
|
1004
|
+
bot: toRaw(bot),
|
|
1005
|
+
user: toRaw(user),
|
|
1006
|
+
packages: toRaw(web.value.pluginCatalog).map((pkg) => ({
|
|
1007
|
+
packageId: pkg.packageId,
|
|
1008
|
+
version: pkg.version,
|
|
1009
|
+
settings: pkg.settings ?? [],
|
|
1010
|
+
capabilities: pkg.capabilities,
|
|
1011
|
+
connectionTypes: pkg.connectionTypes,
|
|
1012
|
+
})),
|
|
1013
|
+
});
|
|
1014
|
+
// `FrockBotWebData`'s source vocabulary is owned outside this lane. Until
|
|
1015
|
+
// it adopts the core's four sources, both inherited choices are its
|
|
1016
|
+
// existing `default` projection; the label still preserves the exact core
|
|
1017
|
+
// source below.
|
|
1018
|
+
web.value.modelSource =
|
|
1019
|
+
effective.source === "bot"
|
|
1020
|
+
? "bot"
|
|
1021
|
+
: effective.source === "none"
|
|
1022
|
+
? "none"
|
|
1023
|
+
: "default";
|
|
1024
|
+
web.value.modelReady = Boolean(
|
|
1025
|
+
effective.binding && effective.binding.state !== "unavailable",
|
|
1032
1026
|
);
|
|
1027
|
+
const connection = effective.binding?.connection;
|
|
1033
1028
|
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,
|
|
1029
|
+
(pkg) => pkg.packageId === effective.binding?.packageId,
|
|
1061
1030
|
);
|
|
1062
1031
|
const catalogModel = connection?.modelCatalog?.models.find(
|
|
1063
|
-
(candidate) =>
|
|
1032
|
+
(candidate) =>
|
|
1033
|
+
candidate.providerModelId === effective.model?.providerModelId,
|
|
1064
1034
|
);
|
|
1065
1035
|
web.value.modelLabel = modelRuntimeLabel({
|
|
1036
|
+
source: effective.source,
|
|
1066
1037
|
modelDisplayName: catalogModel?.displayName,
|
|
1067
|
-
providerModelId: model?.providerModelId,
|
|
1038
|
+
providerModelId: effective.model?.providerModelId,
|
|
1068
1039
|
packageDisplayName: catalogPackage?.displayName,
|
|
1069
1040
|
connectionDisplayName: connection?.displayName,
|
|
1070
|
-
|
|
1041
|
+
failure: effective.binding?.failure,
|
|
1071
1042
|
});
|
|
1072
1043
|
}
|
|
1073
1044
|
|
|
@@ -1102,9 +1073,16 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1102
1073
|
await web.value.loadMcpServers();
|
|
1103
1074
|
}
|
|
1104
1075
|
|
|
1105
|
-
|
|
1076
|
+
type ShellWebData = FrockBotWebData & {
|
|
1077
|
+
saveBotPackageSettings(
|
|
1078
|
+
packageId: string,
|
|
1079
|
+
values: Record<string, PackageSettingValueV1>,
|
|
1080
|
+
): Promise<void>;
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const web: Ref<ShellWebData> = ref({
|
|
1106
1084
|
connection: "ready",
|
|
1107
|
-
modelLabel: "
|
|
1085
|
+
modelLabel: "Model unavailable",
|
|
1108
1086
|
modelReady: false,
|
|
1109
1087
|
modelSource: "none",
|
|
1110
1088
|
settingsAvailable: true,
|
|
@@ -1298,8 +1276,8 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1298
1276
|
)
|
|
1299
1277
|
return;
|
|
1300
1278
|
web.value.botSettings = settings;
|
|
1301
|
-
// The effective model may
|
|
1302
|
-
// needs the User settings too. Loading them never fails this read:
|
|
1279
|
+
// The effective model may come from account or platform state, so Bot
|
|
1280
|
+
// readiness needs the User settings too. Loading them never fails this read:
|
|
1303
1281
|
// `loadUserSettings` reports its own failure.
|
|
1304
1282
|
if (!web.value.userSettings) await web.value.loadUserSettings();
|
|
1305
1283
|
if (
|
|
@@ -1389,153 +1367,23 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1389
1367
|
});
|
|
1390
1368
|
await web.value.loadBotSettings();
|
|
1391
1369
|
},
|
|
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> {
|
|
1370
|
+
async saveBotPackageSettings(
|
|
1371
|
+
packageId: string,
|
|
1372
|
+
values: Record<string, PackageSettingValueV1>,
|
|
1373
|
+
): Promise<void> {
|
|
1423
1374
|
const current = web.value.botSettings;
|
|
1424
1375
|
const botId = web.value.activeBotId;
|
|
1425
1376
|
if (!current || !botId || !ctx.transport.executeConfiguration) {
|
|
1426
1377
|
throw new Error("Settings are unavailable");
|
|
1427
1378
|
}
|
|
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
|
-
const receipt = await ctx.transport.executeConfiguration({
|
|
1500
|
-
schemaVersion: 1,
|
|
1501
|
-
type: "bot/select-model",
|
|
1502
|
-
commandId: crypto.randomUUID(),
|
|
1503
|
-
botId,
|
|
1504
|
-
expectedRevision: current.revision,
|
|
1505
|
-
model,
|
|
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
1379
|
const receipt = await ctx.transport.executeConfiguration({
|
|
1533
1380
|
schemaVersion: 1,
|
|
1534
|
-
type: "bot/
|
|
1381
|
+
type: "bot/set-package-settings",
|
|
1535
1382
|
commandId: crypto.randomUUID(),
|
|
1536
1383
|
botId,
|
|
1537
1384
|
expectedRevision: current.revision,
|
|
1538
|
-
|
|
1385
|
+
packageId,
|
|
1386
|
+
values,
|
|
1539
1387
|
});
|
|
1540
1388
|
await web.value.loadBotSettings();
|
|
1541
1389
|
if (receipt.status === "rejected") throw new Error(receipt.failure);
|
|
@@ -1585,30 +1433,6 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1585
1433
|
});
|
|
1586
1434
|
await web.value.loadUserSettings();
|
|
1587
1435
|
},
|
|
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
1436
|
async loadPluginCatalog(): Promise<void> {
|
|
1613
1437
|
if (
|
|
1614
1438
|
!ctx.transport.readApplicationManifest ||
|
|
@@ -1846,7 +1670,10 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1846
1670
|
// Enablement is projected onto the installation row the Plugins surface
|
|
1847
1671
|
// renders, so the toggle reads back what the User authority recorded.
|
|
1848
1672
|
await web.value.loadPluginCatalog();
|
|
1849
|
-
if (receipt.status === "rejected")
|
|
1673
|
+
if (receipt.status === "rejected") {
|
|
1674
|
+
web.value.settingsError = receipt.failure;
|
|
1675
|
+
throw new Error(receipt.failure);
|
|
1676
|
+
}
|
|
1850
1677
|
},
|
|
1851
1678
|
async savePackageSettings(
|
|
1852
1679
|
packageId: string,
|
|
@@ -2187,7 +2014,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2187
2014
|
runId: pendingRunId,
|
|
2188
2015
|
role: "assistant",
|
|
2189
2016
|
text: aborted
|
|
2190
|
-
? "Request stopped locally;
|
|
2017
|
+
? "Request stopped locally; checking whether it started."
|
|
2191
2018
|
: "Confirming whether this Turn was admitted.",
|
|
2192
2019
|
at: optimisticAt,
|
|
2193
2020
|
status: "interrupted",
|
|
@@ -2246,7 +2073,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2246
2073
|
web.value.activeRun = {
|
|
2247
2074
|
runId,
|
|
2248
2075
|
status: "running",
|
|
2249
|
-
message: "Reconciliation requested;
|
|
2076
|
+
message: "Reconciliation requested; checking progress.",
|
|
2250
2077
|
canResume: false,
|
|
2251
2078
|
};
|
|
2252
2079
|
const botId = web.value.activeBotId;
|
|
@@ -2314,11 +2141,13 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2314
2141
|
async abort() {
|
|
2315
2142
|
activeRequest?.abort();
|
|
2316
2143
|
},
|
|
2317
|
-
})
|
|
2144
|
+
} satisfies Partial<ShellWebData>) as unknown as Ref<ShellWebData>;
|
|
2318
2145
|
|
|
2319
2146
|
return [
|
|
2320
2147
|
ctx.provide(clientSurfaceRegistryKey, surfaces),
|
|
2321
|
-
|
|
2148
|
+
// The shared client projection is updated by the contracts lane. This cast
|
|
2149
|
+
// is the seam between its retired methods and this lane's replacement.
|
|
2150
|
+
ctx.provide(frockBotWebDataKey, web as unknown as Ref<FrockBotWebData>),
|
|
2322
2151
|
ctx.slot({
|
|
2323
2152
|
slot: "authenticated-root",
|
|
2324
2153
|
order: 10_000,
|
|
@@ -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
|
});
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The model line the shell shows above the composer.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The model line the shell shows above the composer. Platform choices read as
|
|
3
|
+
* the model itself; opting into an account choice or Bot override makes that
|
|
4
|
+
* distinction visible. A resolver failure is already the backend's complete,
|
|
5
|
+
* repairable explanation, so the client presents it verbatim.
|
|
6
6
|
*/
|
|
7
7
|
export function modelRuntimeLabel(input: {
|
|
8
|
+
source: "bot" | "account" | "platform" | "none";
|
|
8
9
|
modelDisplayName?: string;
|
|
9
10
|
providerModelId?: string;
|
|
10
11
|
packageDisplayName?: string;
|
|
11
12
|
connectionDisplayName?: string;
|
|
12
|
-
|
|
13
|
+
failure?: string;
|
|
13
14
|
}): string {
|
|
14
|
-
if (
|
|
15
|
+
if (input.failure) return input.failure;
|
|
16
|
+
if (input.source === "none" || !input.providerModelId) {
|
|
17
|
+
return "Model unavailable";
|
|
18
|
+
}
|
|
15
19
|
const model =
|
|
16
20
|
input.modelDisplayName ?? input.providerModelId ?? "Connected model";
|
|
17
21
|
const provider = input.packageDisplayName ?? input.connectionDisplayName;
|
|
18
|
-
|
|
22
|
+
const runtime = provider ? `${model} · ${provider}` : model;
|
|
23
|
+
if (input.source === "bot") return `${runtime} · Bot override`;
|
|
24
|
+
if (input.source === "account") return `${runtime} · Account model`;
|
|
25
|
+
return runtime;
|
|
19
26
|
}
|
package/src/client/styles.css
CHANGED
|
@@ -175,27 +175,6 @@
|
|
|
175
175
|
text-overflow: ellipsis;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
.model-setup-link {
|
|
179
|
-
width: max-content;
|
|
180
|
-
max-width: 100%;
|
|
181
|
-
margin-top: 3px;
|
|
182
|
-
overflow: hidden;
|
|
183
|
-
border: 0;
|
|
184
|
-
padding: 0;
|
|
185
|
-
color: var(--frock-action-primary);
|
|
186
|
-
background: transparent;
|
|
187
|
-
font: inherit;
|
|
188
|
-
font-size: var(--frock-text-sm);
|
|
189
|
-
text-align: left;
|
|
190
|
-
text-overflow: ellipsis;
|
|
191
|
-
white-space: nowrap;
|
|
192
|
-
cursor: pointer;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.model-setup-link:hover {
|
|
196
|
-
color: var(--frock-action-primary-hover);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
178
|
/*
|
|
200
179
|
* The panel collapse control is pinned to the window's trailing edge. Feature
|
|
201
180
|
* actions live in the panel's own header.
|
|
@@ -570,12 +549,6 @@
|
|
|
570
549
|
0 0 0 3px var(--frock-focus-ring);
|
|
571
550
|
}
|
|
572
551
|
|
|
573
|
-
.composer-model-setup {
|
|
574
|
-
min-width: 0;
|
|
575
|
-
flex: 1;
|
|
576
|
-
align-self: center;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
552
|
.composer textarea {
|
|
580
553
|
box-sizing: border-box;
|
|
581
554
|
width: 100%;
|
|
@@ -23,11 +23,11 @@ describe("settings link scheme", () => {
|
|
|
23
23
|
test("renders against an origin when one is supplied", () => {
|
|
24
24
|
expect(
|
|
25
25
|
settingsLinkV1({
|
|
26
|
-
anchor: "bot-
|
|
26
|
+
anchor: "bot-name",
|
|
27
27
|
botId: "alpha",
|
|
28
28
|
origin: "https://app.example/",
|
|
29
29
|
}),
|
|
30
|
-
).toBe("https://app.example/?bot=alpha&settings=bot-settings#bot-
|
|
30
|
+
).toBe("https://app.example/?bot=alpha&settings=bot-settings#bot-name");
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test("refuses an anchor this build does not ship", () => {
|
|
@@ -36,14 +36,6 @@ describe("settings link scheme", () => {
|
|
|
36
36
|
);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test("renders a Markdown citation a payload can carry verbatim", () => {
|
|
40
|
-
expect(
|
|
41
|
-
renderSettingsLinkV1({ anchor: "bot-capabilities", botId: "alpha" }),
|
|
42
|
-
).toBe(
|
|
43
|
-
"[Capability Assignments](/?bot=alpha&settings=bot-settings#bot-capabilities)",
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
39
|
test("decodes an absolute link back to its surface, anchor and Bot", () => {
|
|
48
40
|
expect(
|
|
49
41
|
decodeSettingsLinkV1(
|
package/src/settings-links.ts
CHANGED
|
@@ -108,18 +108,6 @@ export const SETTINGS_ANCHORS_V1: readonly SettingsAnchorV1[] = [
|
|
|
108
108
|
label: "Waiting on you",
|
|
109
109
|
scope: "bot",
|
|
110
110
|
},
|
|
111
|
-
{
|
|
112
|
-
anchor: "bot-model",
|
|
113
|
-
surface: "bot-settings",
|
|
114
|
-
label: "Model",
|
|
115
|
-
scope: "bot",
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
anchor: "bot-capabilities",
|
|
119
|
-
surface: "bot-settings",
|
|
120
|
-
label: "Capability Assignments",
|
|
121
|
-
scope: "bot",
|
|
122
|
-
},
|
|
123
111
|
{
|
|
124
112
|
anchor: "bot-routines",
|
|
125
113
|
surface: "bot-settings",
|
|
@@ -194,7 +182,7 @@ export const SETTINGS_ANCHORS_V1: readonly SettingsAnchorV1[] = [
|
|
|
194
182
|
{
|
|
195
183
|
anchor: "user-connections",
|
|
196
184
|
surface: "connections",
|
|
197
|
-
label: "
|
|
185
|
+
label: "Connectors",
|
|
198
186
|
scope: "user",
|
|
199
187
|
},
|
|
200
188
|
{
|