@frockbot/plugin-shell 0.1.3 → 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.
@@ -25,12 +25,11 @@ import type {
25
25
  BotProfile,
26
26
  BotProfilePatchV1,
27
27
  BotSettingsViewV1,
28
- ConfigurationCommandV1,
29
28
  JsonValue,
30
- ModelAssignment,
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 accepted; waiting for durable settlement.",
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
- // A Package with neither a Connection Type nor a Capability contributes
664
- // nothing the Plugins surface can install or assign. A Capability that
665
- // takes no Connection still counts: a tool Package a User installs and
666
- // assigns without any credential is exactly that shape.
667
- if (connectionTypes.length === 0 && decodedCapabilities.length === 0) {
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
- // User-scoped settings only: a `bot`-scoped one is not the Plugins
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
- const model = bot?.model ?? user?.newBotModelTemplate;
1025
- web.value.modelSource = bot?.model ? "bot" : model ? "default" : "none";
1026
- const connection = (user?.connections ?? []).find(
1027
- (candidate) => candidate.connectionId === model?.connectionId,
1028
- );
1029
- const packageInstalled = (user?.packages ?? []).some(
1030
- (pkg) =>
1031
- pkg.packageId === connection?.packageId && pkg.state === "installed",
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 === connection?.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) => candidate.providerModelId === model?.providerModelId,
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
- hasModel: Boolean(model),
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
- const web = ref<FrockBotWebData>({
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: "No default model",
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 be the User's default, so Bot readiness
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 assignCapability(assignment): Promise<void> {
1393
- const current = web.value.botSettings;
1394
- const botId = web.value.activeBotId;
1395
- if (!current || !botId || !ctx.transport.executeConfiguration) {
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/unbind-model",
1381
+ type: "bot/set-package-settings",
1535
1382
  commandId: crypto.randomUUID(),
1536
1383
  botId,
1537
1384
  expectedRevision: current.revision,
1538
- assignmentId: assignment.assignmentId,
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") throw new Error(receipt.failure);
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; admission may still be durable."
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; waiting for durable progress.",
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
- ctx.provide(frockBotWebDataKey, web),
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 the model and its provider Package", () => {
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("reads the same whether the model is the Bot's or the User default", () => {
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("Llama 3 · Ollama Cloud");
31
- expect(modelRuntimeLabel({ ...label, hasModel: false })).toBe(
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. It names the model the
3
- * Bot actually runs on, whether that model is the Bot's own override or the
4
- * User's default: a Bot that "just works" does not advertise where its
5
- * settings came from.
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
- hasModel: boolean;
13
+ failure?: string;
13
14
  }): string {
14
- if (!input.hasModel) return "No default model";
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
- return provider ? `${model} · ${provider}` : model;
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
  }
@@ -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-model",
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-model");
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(
@@ -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: "Connections",
185
+ label: "Connectors",
198
186
  scope: "user",
199
187
  },
200
188
  {