@capybara-hq/kernel 0.1.0-rc.14 → 0.1.0-rc.16

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.
Files changed (107) hide show
  1. package/dist/admin/admin.schemas.d.ts +5 -13
  2. package/dist/catalog/admin-catalog.controller.d.ts +27 -4
  3. package/dist/catalog/admin-catalog.controller.js +53 -5
  4. package/dist/catalog/appearance.schemas.d.ts +10 -0
  5. package/dist/catalog/appearance.schemas.js +20 -0
  6. package/dist/catalog/appearance.service.d.ts +105 -0
  7. package/dist/catalog/appearance.service.js +306 -0
  8. package/dist/catalog/catalog-import.module.js +3 -0
  9. package/dist/catalog/catalog-items.d.ts +11 -0
  10. package/dist/catalog/catalog-items.js +21 -0
  11. package/dist/catalog/catalog-scope.service.d.ts +7 -0
  12. package/dist/catalog/catalog-scope.service.js +55 -15
  13. package/dist/catalog/catalog.service.js +10 -2
  14. package/dist/catalog/cli.js +3 -0
  15. package/dist/catalog/marketplaces.service.d.ts +1 -1
  16. package/dist/catalog/org-extension.controller.d.ts +27 -7
  17. package/dist/catalog/org-extension.controller.js +48 -6
  18. package/dist/catalog/plugin-import.d.ts +28 -4
  19. package/dist/catalog/plugin-import.js +95 -19
  20. package/dist/catalog/plugin-package.d.ts +4 -2
  21. package/dist/catalog/plugin-package.js +29 -3
  22. package/dist/channels/channels.schemas.d.ts +4 -0
  23. package/dist/channels/channels.schemas.js +2 -0
  24. package/dist/channels/connections.controller.d.ts +5 -3
  25. package/dist/channels/connections.controller.js +6 -4
  26. package/dist/channels/connections.service.d.ts +26 -0
  27. package/dist/channels/connections.service.js +79 -6
  28. package/dist/client/client-access.service.d.ts +101 -0
  29. package/dist/client/client-access.service.js +294 -0
  30. package/dist/client/client-builds.service.d.ts +109 -0
  31. package/dist/client/client-builds.service.js +229 -0
  32. package/dist/client/client-delivery.controller.d.ts +25 -0
  33. package/dist/client/client-delivery.controller.js +66 -0
  34. package/dist/client/client-files.d.ts +48 -0
  35. package/dist/client/client-files.js +136 -0
  36. package/dist/client/client-reports.cron.d.ts +13 -0
  37. package/dist/client/client-reports.cron.js +42 -0
  38. package/dist/client/client-reports.service.d.ts +30 -0
  39. package/dist/client/client-reports.service.js +102 -0
  40. package/dist/client/client.controller.d.ts +45 -0
  41. package/dist/client/client.controller.js +121 -0
  42. package/dist/client/client.module.d.ts +7 -0
  43. package/dist/client/client.module.js +26 -0
  44. package/dist/client/client.schemas.d.ts +55 -0
  45. package/dist/client/client.schemas.js +27 -0
  46. package/dist/ctx/create-ctx.d.ts +4 -3
  47. package/dist/ctx/create-ctx.js +4 -3
  48. package/dist/db/kernel-schema.d.ts +47 -0
  49. package/dist/errors.d.ts +29 -0
  50. package/dist/errors.js +29 -0
  51. package/dist/extensions/admin-ops.d.ts +4 -1
  52. package/dist/extensions/admin-ops.js +22 -1
  53. package/dist/extensions/admin-view.d.ts +8 -0
  54. package/dist/extensions/admin-view.js +78 -0
  55. package/dist/extensions/blobs.d.ts +14 -1
  56. package/dist/extensions/blobs.js +48 -5
  57. package/dist/extensions/guide-places.d.ts +3 -1
  58. package/dist/extensions/guide-places.js +1 -1
  59. package/dist/extensions/host.d.ts +6 -1
  60. package/dist/extensions/host.js +20 -9
  61. package/dist/extensions/package.d.ts +4 -3
  62. package/dist/extensions/package.js +5 -1
  63. package/dist/extensions/permission-set.d.ts +19 -3
  64. package/dist/extensions/permission-set.js +19 -2
  65. package/dist/extensions/permission-view.d.ts +38 -7
  66. package/dist/extensions/permission-view.js +23 -7
  67. package/dist/extensions/sources.d.ts +6 -0
  68. package/dist/extensions/sources.js +2 -0
  69. package/dist/extensions/state.module.js +9 -1
  70. package/dist/http/api.module.js +10 -0
  71. package/dist/identity/settings.d.ts +41 -26
  72. package/dist/identity/settings.js +87 -4
  73. package/dist/infra/throttler/named-throttlers.d.ts +4 -0
  74. package/dist/infra/throttler/named-throttlers.js +14 -0
  75. package/dist/kernel.module.js +3 -0
  76. package/dist/orgs/settings.service.d.ts +2 -26
  77. package/dist/platform/exclusions.js +27 -1
  78. package/dist/platform/ops/admin.ops.js +17 -2
  79. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  80. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  81. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  82. package/dist/platform/ops/org-plugins.ops.js +63 -4
  83. package/dist/platform/ops/org-settings.ops.js +2 -2
  84. package/dist/platform/ops/orgs.ops.js +1 -0
  85. package/dist/platform/ops/ui.ops.js +1 -1
  86. package/dist/platform/platform-ops.module.js +2 -0
  87. package/dist/platform/route-schemas.js +3 -0
  88. package/dist/roles/worker.module.js +4 -0
  89. package/dist/themes/render-css.d.ts +2 -1
  90. package/dist/themes/render-css.js +5 -2
  91. package/dist/themes/theme-delivery.service.d.ts +4 -2
  92. package/dist/themes/theme-delivery.service.js +8 -3
  93. package/dist/themes/theme-state.d.ts +28 -7
  94. package/dist/themes/theme-state.js +24 -6
  95. package/dist/themes/themes-public.controller.d.ts +7 -2
  96. package/dist/themes/themes-public.controller.js +11 -5
  97. package/dist/themes/themes.service.js +2 -2
  98. package/dist/tools/pipeline.d.ts +3 -2
  99. package/dist/ui/arrangement.d.ts +41 -50
  100. package/dist/ui/arrangement.js +88 -15
  101. package/dist/ui/registry.d.ts +13 -3
  102. package/dist/ui/registry.js +15 -4
  103. package/dist/ui/ui.schemas.d.ts +4 -0
  104. package/dist/ui/views.d.ts +36 -9
  105. package/dist/ui/views.js +58 -28
  106. package/package.json +3 -3
  107. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -23,9 +23,14 @@ let ThemeDeliveryController = class ThemeDeliveryController {
23
23
  constructor(delivery) {
24
24
  this.delivery = delivery;
25
25
  }
26
- /** The version's stylesheet: scheme overrides, `@font-face` rules and font stacks. */
27
- async css(id, version, reply) {
28
- const css = await this.delivery.stylesheet(id, version);
26
+ /**
27
+ * The version's stylesheet: scheme overrides, `@font-face` rules and font stacks, in the render
28
+ * revision `revision` (`r<THEME_CSS_REVISION>`; only the current one is served). The URL without
29
+ * a revision (before revision 2) has no route: 404, so a browser's cached unlayered copy is never
30
+ * answered with the new format under an immutable cache.
31
+ */
32
+ async css(id, version, revision, reply) {
33
+ const css = await this.delivery.stylesheet(id, version, revision);
29
34
  await reply
30
35
  .header("content-type", "text/css; charset=utf-8")
31
36
  .header("x-content-type-options", "nosniff")
@@ -51,10 +56,11 @@ let ThemeDeliveryController = class ThemeDeliveryController {
51
56
  };
52
57
  __decorate([
53
58
  Public(),
54
- Get("theme.css"),
59
+ Get(":revision/theme.css"),
55
60
  __param(0, Param("id")),
56
61
  __param(1, Param("version")),
57
- __param(2, Res())
62
+ __param(2, Param("revision")),
63
+ __param(3, Res())
58
64
  ], ThemeDeliveryController.prototype, "css", null);
59
65
  __decorate([
60
66
  Public(),
@@ -32,7 +32,7 @@ let ThemesService = class ThemesService {
32
32
  if (!d)
33
33
  continue;
34
34
  out.push({
35
- ...themeRefOf(c.id, d.version, d.definition),
35
+ ...themeRefOf(c.id, d.version, d.definition, d.plugin),
36
36
  swatches: themeSwatches(d.definition),
37
37
  });
38
38
  }
@@ -53,7 +53,7 @@ let ThemesService = class ThemesService {
53
53
  if (id === null || (await eligiblePlatformDefault(this.db.db, id)) === null)
54
54
  return null;
55
55
  const d = (await themeDefinitions(this.db.db, [id])).get(id);
56
- return d ? themeRefOf(id, d.version, d.definition) : null;
56
+ return d ? themeRefOf(id, d.version, d.definition, d.plugin) : null;
57
57
  }
58
58
  /**
59
59
  * The listing, the viewer's resolved theme and the org's setting. Separate reads, not one
@@ -151,8 +151,9 @@ interface PipelineDeps {
151
151
  /** The org's switches of single extension tools: a turned-off one is not called (spec A §7). */
152
152
  readonly switches: Pick<OrgToolSwitches, "isToolOff">;
153
153
  /**
154
- * Ids of an extension's `inline` ui contributions (read live, no alias): a plugin tool's
155
- * `ToolResult.card` must name one of them, otherwise it is dropped with a warning.
154
+ * Ids of an extension's `conversation.tool-result.card` contributions of any kind (view, embed
155
+ * or component; read live, no alias): a plugin tool's `ToolResult.card` must name one of them,
156
+ * otherwise it is dropped with a warning.
156
157
  */
157
158
  inlineViews(plugin: string): ReadonlySet<string>;
158
159
  /**
@@ -1,74 +1,65 @@
1
1
  import { type UiSlotId } from "@capybara-hq/plugin-sdk";
2
2
  import { z } from "zod";
3
3
  import type { UiContributionItem } from "./views.js";
4
- export declare const UiArrangementEntrySchema: z.ZodObject<{
5
- hidden: z.ZodArray<z.ZodString>;
6
- order: z.ZodArray<z.ZodString>;
7
- }, z.core.$strict>;
8
- export type UiArrangementEntry = z.infer<typeof UiArrangementEntrySchema>;
9
- export declare const UiArrangementSchema: z.ZodRecord<z.ZodEnum<{
10
- "agent.detail.section": "agent.detail.section";
11
- "conversation.panel": "conversation.panel";
12
- "conversation.properties.section": "conversation.properties.section";
13
- "conversation.tool-result.card": "conversation.tool-result.card";
14
- "file.preview": "file.preview";
15
- "org.page": "org.page";
16
- "org.settings.section": "org.settings.section";
17
- "platform.page": "platform.page";
18
- }> & z.core.$partial, z.ZodObject<{
19
- hidden: z.ZodArray<z.ZodString>;
20
- order: z.ZodArray<z.ZodString>;
21
- }, z.core.$strict>>;
22
- export type UiArrangement = z.infer<typeof UiArrangementSchema>;
4
+ export type UiListEntry = {
5
+ hidden: string[];
6
+ order: string[];
7
+ };
8
+ export type UiUseEntry = {
9
+ use: string | null;
10
+ };
11
+ /** One slot's entry: `{ hidden, order }` on a list slot, `{ use }` on a single slot. */
12
+ export type UiArrangementEntry = UiListEntry | UiUseEntry;
13
+ export type UiArrangement = Partial<Record<UiSlotId, UiArrangementEntry>>;
14
+ /** Whether a slot is a single (replaceable) slot. */
15
+ export declare const isSingle: (slot: UiSlotId) => boolean;
16
+ export declare const UiArrangementSchema: z.ZodType<UiArrangement>;
23
17
  /** The slots an org arranges: every arrangeable slot but the platform's own. */
24
18
  export declare const ORG_ARRANGEABLE: readonly UiSlotId[];
25
19
  /** An org's patch: a slot's entry replaces the stored one, null removes it. */
26
- export declare const OrgUiArrangementPatchSchema: z.ZodRecord<z.ZodEnum<{
27
- "agent.detail.section": "agent.detail.section";
28
- "conversation.panel": "conversation.panel";
29
- "conversation.properties.section": "conversation.properties.section";
30
- "conversation.tool-result.card": "conversation.tool-result.card";
31
- "file.preview": "file.preview";
32
- "org.page": "org.page";
33
- "org.settings.section": "org.settings.section";
34
- "platform.page": "platform.page";
35
- }> & z.core.$partial, z.ZodNullable<z.ZodObject<{
36
- hidden: z.ZodArray<z.ZodString>;
37
- order: z.ZodArray<z.ZodString>;
38
- }, z.core.$strict>>>;
20
+ export declare const OrgUiArrangementPatchSchema: z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown>>;
39
21
  /** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
40
- export declare const UiArrangementPatchSchema: z.ZodRecord<z.ZodEnum<{
41
- "agent.detail.section": "agent.detail.section";
42
- "conversation.panel": "conversation.panel";
43
- "conversation.properties.section": "conversation.properties.section";
44
- "conversation.tool-result.card": "conversation.tool-result.card";
45
- "file.preview": "file.preview";
46
- "org.page": "org.page";
47
- "org.settings.section": "org.settings.section";
48
- "platform.page": "platform.page";
49
- }> & z.core.$partial, z.ZodNullable<z.ZodObject<{
50
- hidden: z.ZodArray<z.ZodString>;
51
- order: z.ZodArray<z.ZodString>;
52
- }, z.core.$strict>>>;
22
+ export declare const UiArrangementPatchSchema: z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown>>;
53
23
  /** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
54
24
  export declare function mergeArrangement(current: UiArrangement | undefined, patch: Partial<Record<UiSlotId, UiArrangementEntry | null>>): UiArrangement;
55
- /** The entry that applies to a slot: the org's if it arranged the slot, else the platform's. */
25
+ /**
26
+ * The list entry that applies to a slot: the org's if it arranged the slot, else the platform's
27
+ * (a `use` entry, which only a single slot stores, reads as none).
28
+ */
56
29
  export declare function effectiveEntry(org: UiArrangement | undefined, platform: UiArrangement, slot: UiSlotId): {
57
- entry: UiArrangementEntry;
30
+ entry: UiListEntry;
58
31
  source: "org" | "platform" | "none";
59
32
  };
33
+ /**
34
+ * The winner of a single slot (client extensions spec §6.3): the org's entry if it has one, else
35
+ * the platform's, else none. No entry or `use: null` is the built-in (null); a `use` naming a key
36
+ * not in `available` (extension off in the org, contribution dropped, the viewer's role below the
37
+ * slot's) is the built-in too, never the other level's pick. A list slot, or an entry that is not
38
+ * a `use` entry, has no winner.
39
+ */
40
+ export declare function resolveSingleSlot(slot: UiSlotId, orgEntry: UiArrangementEntry | undefined, platformEntry: UiArrangementEntry | undefined, available: ReadonlySet<string>): string | null;
41
+ /** The `use` stored for a single slot: the org's entry if it has one, else the platform's. */
42
+ export declare function storedUse(org: UiArrangement | undefined, platform: UiArrangement, slot: UiSlotId): {
43
+ use: string | null;
44
+ followsPlatform: boolean;
45
+ };
46
+ /** The arrangement key of a contribution: `<plugin>/<id>`. */
47
+ export declare const keyOf: (i: {
48
+ plugin: string;
49
+ id: string;
50
+ }) => string;
60
51
  /**
61
52
  * Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
62
53
  * order, also without an entry (the lists match what the page renders per anchor); within a group
63
54
  * the keys of `entry.order` come first in that order, the rest in base order. Unknown keys are
64
55
  * ignored, so an order never moves an item across anchors.
65
56
  */
66
- export declare function orderItems(items: UiContributionItem[], entry: UiArrangementEntry): UiContributionItem[];
57
+ export declare function orderItems(items: UiContributionItem[], entry: UiListEntry): UiContributionItem[];
67
58
  /** {@link orderItems} without the hidden keys. */
68
- export declare function arrange(items: UiContributionItem[], entry: UiArrangementEntry): UiContributionItem[];
59
+ export declare function arrange(items: UiContributionItem[], entry: UiListEntry): UiContributionItem[];
69
60
  /**
70
61
  * Applies the per-slot entries to a mixed list: each slot's items are arranged and written back
71
62
  * into the positions that slot occupied (a slot without an entry: its base order, grouped by
72
63
  * anchor).
73
64
  */
74
- export declare function arrangeBySlot(items: UiContributionItem[], entryOf: (slot: UiSlotId) => UiArrangementEntry): UiContributionItem[];
65
+ export declare function arrangeBySlot(items: UiContributionItem[], entryOf: (slot: UiSlotId) => UiListEntry): UiContributionItem[];
@@ -1,22 +1,66 @@
1
- import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, uiSlot, } from "@capybara-hq/plugin-sdk";
1
+ import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, UI_SINGLE_SLOTS, uiSlot, } from "@capybara-hq/plugin-sdk";
2
2
  import { z } from "zod";
3
3
  /**
4
- * Arrangement of UI slots (UI slots spec §5): per slot, which contributions are hidden and in
5
- * which order. "Hidden" means not listed: the access gate never reads an arrangement, so a hidden
6
- * contribution's view, action and embed routes keep working for an allowed role.
4
+ * Arrangement of UI slots (UI slots spec §5, client extensions spec §6.3): per list slot, which
5
+ * contributions are hidden and in which order; per single slot, which contribution replaces the
6
+ * built-in (`use`, null = the built-in). "Hidden" means not listed and `use` only picks what is
7
+ * listed: the access gate never reads an arrangement, so a contribution's view, action and embed
8
+ * routes keep working for an allowed role whatever the arrangement says.
7
9
  */
8
- const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE);
10
+ const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE, "must be a contribution key <plugin>/<id>");
9
11
  const LIST_MAX = 200;
10
- export const UiArrangementEntrySchema = z
11
- .object({ hidden: z.array(Key).max(LIST_MAX), order: z.array(Key).max(LIST_MAX) })
12
- .strict();
13
- export const UiArrangementSchema = z.partialRecord(z.enum(UI_ARRANGEABLE_SLOTS), UiArrangementEntrySchema);
12
+ /**
13
+ * A list slot's entry: the keys hidden and the keys ordered first. The refusals name the slot and
14
+ * what it takes (actionable for an admin and the Platform tool's model).
15
+ */
16
+ const listEntrySchema = (slot) => {
17
+ const keys = z
18
+ .array(Key, {
19
+ error: (iss) => iss.input === undefined
20
+ ? `${slot} is a list slot: give an array of contribution keys <plugin>/<id>`
21
+ : undefined,
22
+ })
23
+ .max(LIST_MAX);
24
+ return z.strictObject({ hidden: keys, order: keys }, {
25
+ error: (iss) => iss.code === "unrecognized_keys"
26
+ ? `${slot} is a list slot: give { hidden, order }; use is only for the single slots (${UI_SINGLE_SLOTS.join(", ")})`
27
+ : undefined,
28
+ });
29
+ };
30
+ /** A single slot's entry: the contribution that replaces the built-in, or null for the built-in. */
31
+ const useEntrySchema = (slot) => z.strictObject({
32
+ use: z.union([Key, z.null()], {
33
+ error: (iss) => iss.input === undefined
34
+ ? `${slot} needs use: a contribution key <plugin>/<id>, or null for the built-in`
35
+ : undefined,
36
+ }),
37
+ }, {
38
+ error: (iss) => iss.code === "unrecognized_keys"
39
+ ? `${slot} is a single slot: give { use } (a contribution key <plugin>/<id>, or null for the built-in); hidden and order are only for list slots`
40
+ : undefined,
41
+ });
42
+ /** Whether a slot is a single (replaceable) slot. */
43
+ export const isSingle = (slot) => UI_SINGLE_SLOTS.includes(slot);
44
+ /**
45
+ * An object over `slots`, each optional and of its kind's entry (nullable in a patch); unknown
46
+ * slots are refused, and so are `use` on a list slot and `hidden` / `order` on a single slot (400
47
+ * `validation_failed` on the settings routes).
48
+ */
49
+ function arrangementShape(slots, nullable) {
50
+ const shape = {};
51
+ for (const slot of slots) {
52
+ const entry = isSingle(slot) ? useEntrySchema(slot) : listEntrySchema(slot);
53
+ shape[slot] = (nullable ? entry.nullable() : entry).optional();
54
+ }
55
+ return z.object(shape).strict();
56
+ }
57
+ export const UiArrangementSchema = arrangementShape(UI_ARRANGEABLE_SLOTS, false);
14
58
  /** The slots an org arranges: every arrangeable slot but the platform's own. */
15
59
  export const ORG_ARRANGEABLE = UI_ARRANGEABLE_SLOTS.filter((s) => s !== "platform.page");
16
60
  /** An org's patch: a slot's entry replaces the stored one, null removes it. */
17
- export const OrgUiArrangementPatchSchema = z.partialRecord(z.enum(ORG_ARRANGEABLE), UiArrangementEntrySchema.nullable());
61
+ export const OrgUiArrangementPatchSchema = arrangementShape(ORG_ARRANGEABLE, true);
18
62
  /** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
19
- export const UiArrangementPatchSchema = z.partialRecord(z.enum(UI_ARRANGEABLE_SLOTS), UiArrangementEntrySchema.nullable());
63
+ export const UiArrangementPatchSchema = arrangementShape(UI_ARRANGEABLE_SLOTS, true);
20
64
  /** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
21
65
  export function mergeArrangement(current, patch) {
22
66
  const merged = { ...current };
@@ -29,17 +73,46 @@ export function mergeArrangement(current, patch) {
29
73
  return merged;
30
74
  }
31
75
  const NONE = { hidden: [], order: [] };
32
- /** The entry that applies to a slot: the org's if it arranged the slot, else the platform's. */
76
+ const asList = (e) => ("hidden" in e ? e : NONE);
77
+ /**
78
+ * The list entry that applies to a slot: the org's if it arranged the slot, else the platform's
79
+ * (a `use` entry, which only a single slot stores, reads as none).
80
+ */
33
81
  export function effectiveEntry(org, platform, slot) {
34
82
  const o = org?.[slot];
35
83
  if (o !== undefined)
36
- return { entry: o, source: "org" };
84
+ return { entry: asList(o), source: "org" };
37
85
  const p = platform[slot];
38
86
  if (p !== undefined)
39
- return { entry: p, source: "platform" };
87
+ return { entry: asList(p), source: "platform" };
40
88
  return { entry: NONE, source: "none" };
41
89
  }
42
- const keyOf = (i) => `${i.plugin}/${i.id}`;
90
+ /**
91
+ * The winner of a single slot (client extensions spec §6.3): the org's entry if it has one, else
92
+ * the platform's, else none. No entry or `use: null` is the built-in (null); a `use` naming a key
93
+ * not in `available` (extension off in the org, contribution dropped, the viewer's role below the
94
+ * slot's) is the built-in too, never the other level's pick. A list slot, or an entry that is not
95
+ * a `use` entry, has no winner.
96
+ */
97
+ export function resolveSingleSlot(slot, orgEntry, platformEntry, available) {
98
+ if (!isSingle(slot))
99
+ return null;
100
+ const entry = orgEntry ?? platformEntry;
101
+ if (entry === undefined || !("use" in entry) || entry.use === null)
102
+ return null;
103
+ return available.has(entry.use) ? entry.use : null;
104
+ }
105
+ /** The `use` stored for a single slot: the org's entry if it has one, else the platform's. */
106
+ export function storedUse(org, platform, slot) {
107
+ const o = org?.[slot];
108
+ const e = o ?? platform[slot];
109
+ return {
110
+ use: e !== undefined && "use" in e ? e.use : null,
111
+ followsPlatform: o === undefined,
112
+ };
113
+ }
114
+ /** The arrangement key of a contribution: `<plugin>/<id>`. */
115
+ export const keyOf = (i) => `${i.plugin}/${i.id}`;
43
116
  /**
44
117
  * Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
45
118
  * order, also without an entry (the lists match what the page renders per anchor); within a group
@@ -45,19 +45,29 @@ export declare const uiFrame: (m: UiBusMessage) => string;
45
45
  /**
46
46
  * 单个插件在 setup 期间注册的视图与动作:进程内的静态登记(同 `ctx.tools.register`),
47
47
  * 不是跨请求的共享状态——各副本加载同一插件得到同样的登记。
48
- * id 必须是 `permissions.ui.views` 里的(与清单 `ui` 里 kind view 的贡献一一对应,`defineExtension` 已交叉核对)。
48
+ * The id must be in `permissions.ui.views` or `permissions.ui.components` (a component's server
49
+ * view, client extensions spec §5.2); `defineExtension` already cross-checked them with the
50
+ * manifest `ui`.
49
51
  */
50
52
  export declare class PluginUiRegistration {
51
53
  private readonly manifest;
52
- /** 越权(id 未在 `permissions.ui.views` 声明):抛 ExtensionPermissionError 并写审计。 */
54
+ /**
55
+ * Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
56
+ * throws ExtensionPermissionError and writes an audit entry.
57
+ */
53
58
  private readonly deny;
54
59
  readonly views: Map<string, UiRender>;
55
60
  readonly actions: Map<string, UiActionHandler>;
56
61
  private readonly declaredViews;
57
62
  private readonly contributions;
63
+ /** The ids of the manifest's `component` contributions: their permission is `ui.components`. */
64
+ private readonly components;
58
65
  private sealed;
59
66
  constructor(manifest: ExtensionManifest,
60
- /** 越权(id 未在 `permissions.ui.views` 声明):抛 ExtensionPermissionError 并写审计。 */
67
+ /**
68
+ * Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
69
+ * throws ExtensionPermissionError and writes an audit entry.
70
+ */
61
71
  deny: (item: string) => never);
62
72
  /** setup 结束后调用:此后 view / action 抛错。 */
63
73
  seal(): void;
@@ -31,7 +31,9 @@ export const uiFrame = (m) => `event: ui\ndata: ${JSON.stringify({ plugin: m.plu
31
31
  /**
32
32
  * 单个插件在 setup 期间注册的视图与动作:进程内的静态登记(同 `ctx.tools.register`),
33
33
  * 不是跨请求的共享状态——各副本加载同一插件得到同样的登记。
34
- * id 必须是 `permissions.ui.views` 里的(与清单 `ui` 里 kind view 的贡献一一对应,`defineExtension` 已交叉核对)。
34
+ * The id must be in `permissions.ui.views` or `permissions.ui.components` (a component's server
35
+ * view, client extensions spec §5.2); `defineExtension` already cross-checked them with the
36
+ * manifest `ui`.
35
37
  */
36
38
  export class PluginUiRegistration {
37
39
  manifest;
@@ -40,14 +42,21 @@ export class PluginUiRegistration {
40
42
  actions = new Map();
41
43
  declaredViews;
42
44
  contributions;
45
+ /** The ids of the manifest's `component` contributions: their permission is `ui.components`. */
46
+ components;
43
47
  sealed = false;
44
48
  constructor(manifest,
45
- /** 越权(id 未在 `permissions.ui.views` 声明):抛 ExtensionPermissionError 并写审计。 */
49
+ /**
50
+ * Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
51
+ * throws ExtensionPermissionError and writes an audit entry.
52
+ */
46
53
  deny) {
47
54
  this.manifest = manifest;
48
55
  this.deny = deny;
49
- this.declaredViews = new Set(manifest.permissions?.ui?.views ?? []);
56
+ const ui = manifest.permissions?.ui;
57
+ this.declaredViews = new Set([...(ui?.views ?? []), ...(ui?.components ?? [])]);
50
58
  this.contributions = new Set((manifest.ui ?? []).map((c) => c.id));
59
+ this.components = new Set((manifest.ui ?? []).filter((c) => c.kind === "component").map((c) => c.id));
51
60
  }
52
61
  /** setup 结束后调用:此后 view / action 抛错。 */
53
62
  seal() {
@@ -99,7 +108,9 @@ export class PluginUiRegistration {
99
108
  throw new Error(`plugin ${name}: ui.${what} is only allowed during setup; setup has completed`);
100
109
  }
101
110
  if (typeof id !== "string" || !this.declaredViews.has(id)) {
102
- this.deny(`permissions.ui.views ${String(id)}`);
111
+ // name the list the id belongs in: a component's server view is declared under components
112
+ const list = typeof id === "string" && this.components.has(id) ? "components" : "views";
113
+ this.deny(`permissions.ui.${list} ${String(id)}`);
103
114
  }
104
115
  if (typeof fn !== "function") {
105
116
  throw new Error(`plugin ${name}: ui.${what} "${id}" handler must be a function`);
@@ -9,6 +9,8 @@ export declare const ContributionParams: z.ZodObject<{
9
9
  export declare const ContributionsQuery: z.ZodObject<{
10
10
  slot: z.ZodOptional<z.ZodEnum<{
11
11
  "agent.detail.section": "agent.detail.section";
12
+ "conversation.composer.frame": "conversation.composer.frame";
13
+ "conversation.empty": "conversation.empty";
12
14
  "conversation.panel": "conversation.panel";
13
15
  "conversation.properties.section": "conversation.properties.section";
14
16
  "conversation.tool-result.card": "conversation.tool-result.card";
@@ -16,6 +18,8 @@ export declare const ContributionsQuery: z.ZodObject<{
16
18
  "org.page": "org.page";
17
19
  "org.settings.section": "org.settings.section";
18
20
  "platform.page": "platform.page";
21
+ "shell.brand": "shell.brand";
22
+ "shell.overlay": "shell.overlay";
19
23
  }>>;
20
24
  }, z.core.$strip>;
21
25
  export declare const ViewQuery: z.ZodObject<{
@@ -24,14 +24,31 @@ export interface UiContributionItem {
24
24
  /** From `UI_ICONS`; `puzzle` when the manifest names none. */
25
25
  icon: UiIcon;
26
26
  }
27
- /** One slot of an arrangement listing: where its entry comes from and its items in order. */
28
- export interface UiArrangementSlot {
27
+ /** A list slot of an arrangement listing: where its entry comes from and its items in order. */
28
+ export interface UiArrangementListSlot {
29
29
  slot: UiSlotId;
30
30
  source: "org" | "platform" | "none";
31
31
  items: (UiContributionItem & {
32
32
  hidden: boolean;
33
33
  })[];
34
34
  }
35
+ /**
36
+ * A single slot of an arrangement listing (client extensions spec §6.3): the contributions that may
37
+ * replace the built-in, the stored `use` (null = the built-in; it may name a key that is not a
38
+ * candidate any more, which renders the built-in) and whether it is the platform's (the org has no
39
+ * entry; always false on the platform's own listing).
40
+ */
41
+ export interface UiArrangementSingleSlot {
42
+ slot: UiSlotId;
43
+ candidates: {
44
+ key: string;
45
+ title: UiContribution["title"];
46
+ }[];
47
+ use: string | null;
48
+ followsPlatform: boolean;
49
+ }
50
+ /** One slot of an arrangement listing. */
51
+ export type UiArrangementSlot = UiArrangementListSlot | UiArrangementSingleSlot;
35
52
  /** 嵌入应用的签发目标:已核对组织启用与对话访问。 */
36
53
  export interface UiEmbedTarget {
37
54
  plugin: string;
@@ -101,6 +118,8 @@ export declare function dropForeignOpens(view: UiView, panels: ReadonlySet<strin
101
118
  * org; org: the role alone); refused is 404 `ui.not_found`, the same as not existing;
102
119
  * - a view takes only the context id of its scope (`conversationId` / `agentId`): a missing one or
103
120
  * one of another scope is 400 `validation_failed`;
121
+ * - a component's server view (`ctx.ui.view(<component id>)`) is served like a view of its slot;
122
+ * only its views carry `data` (a plain view's `data` is 502 `ui.invalid_view`);
104
123
  * - 调用带 5 秒时限与中止信号:render 抛错 502 `ui.render_failed`、action 抛错 502 `ui.action_failed`、
105
124
  * 超时 504 `ui.timeout`、返回非法视图 502
106
125
  * `ui.invalid_view`,都记告警日志、不写对话日志(这些不是模型可见的内容)。
@@ -111,27 +130,35 @@ export declare class UiViews {
111
130
  constructor(host: ExtensionHost, conversations: ConversationsService, previewers: PreviewerRegistry, db: KernelDb, log: Pick<Logger, "warn">, effective: Pick<EffectiveConfigs, "get" | "platform">);
112
131
  /**
113
132
  * The contributions the role may use in the org, of one slot or of every org-reachable slot:
114
- * the extension enabled and loaded, a view's render registered, the slot's `minRole`
115
- * ({@link canUseSlot}, so never `platform.page`); no alias, no `file.preview` contribution (the
116
- * file previewers list those). Base order (spec §5): extension name, then manifest order.
133
+ * the extension enabled and loaded, a view's render registered (a component is listed with or
134
+ * without a server view), the slot's `minRole` ({@link canUseSlot}, so never `platform.page`);
135
+ * no alias, no `file.preview` contribution (the file previewers list those). Base order (spec
136
+ * §5): extension name, then manifest order. A single slot lists at most its winner
137
+ * ({@link resolveSingleSlot} over what this role may use: none until one is chosen, and the web
138
+ * renders the built-in); display only, the routes that serve a contribution never read it. The
139
+ * winner is listed whether or not its client code may load (kill switch, per-plugin disable,
140
+ * client API support): the web renders the built-in when the org's client set has no such
141
+ * component, and any other consumer must do the same.
117
142
  */
118
143
  contributions(orgId: string, role: OrgRole, slot?: UiSlotId): Promise<UiContributionItem[]>;
119
144
  /** {@link contributions} before the arrangement: what the role may use, in base order. */
120
145
  private orgBase;
121
146
  /**
122
147
  * The arrangement page of an org: every org-arrangeable slot with the contributions the role may
123
- * use, in effective order with the hidden ones in place, and where the entry comes from.
148
+ * use: a list slot in effective order with the hidden ones in place, and where the entry comes
149
+ * from; a single slot with its candidates, the stored `use` and whether it follows the platform.
124
150
  */
125
151
  arrangementView(orgId: string, role: OrgRole): Promise<UiArrangementSlot[]>;
126
152
  /**
127
153
  * The platform's arrangement page: every arrangeable slot with the contributions of every running
128
- * extension (whatever its org enablement), ordered by the platform entry, hidden ones in place.
154
+ * extension (whatever its org enablement): a list slot ordered by the platform entry, hidden ones
155
+ * in place; a single slot with its candidates and the platform's `use`.
129
156
  */
130
157
  platformArrangementView(): Promise<UiArrangementSlot[]>;
131
158
  /**
132
159
  * The `platform.page` contributions of the running extensions, for the admin console (super
133
- * admins; no org enablement applies): no alias, a view's render registered. Base order like
134
- * {@link contributions}.
160
+ * admins; no org enablement applies): no alias, a view's render registered (a component is
161
+ * listed with or without a server view). Base order like {@link contributions}.
135
162
  */
136
163
  platformContributions(): Promise<UiContributionItem[]>;
137
164
  /**