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

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 (99) 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/client/client-access.service.d.ts +101 -0
  23. package/dist/client/client-access.service.js +294 -0
  24. package/dist/client/client-builds.service.d.ts +109 -0
  25. package/dist/client/client-builds.service.js +229 -0
  26. package/dist/client/client-delivery.controller.d.ts +25 -0
  27. package/dist/client/client-delivery.controller.js +66 -0
  28. package/dist/client/client-files.d.ts +48 -0
  29. package/dist/client/client-files.js +136 -0
  30. package/dist/client/client-reports.cron.d.ts +13 -0
  31. package/dist/client/client-reports.cron.js +42 -0
  32. package/dist/client/client-reports.service.d.ts +30 -0
  33. package/dist/client/client-reports.service.js +102 -0
  34. package/dist/client/client.controller.d.ts +45 -0
  35. package/dist/client/client.controller.js +121 -0
  36. package/dist/client/client.module.d.ts +7 -0
  37. package/dist/client/client.module.js +26 -0
  38. package/dist/client/client.schemas.d.ts +55 -0
  39. package/dist/client/client.schemas.js +27 -0
  40. package/dist/ctx/create-ctx.d.ts +4 -3
  41. package/dist/ctx/create-ctx.js +4 -3
  42. package/dist/db/kernel-schema.d.ts +47 -0
  43. package/dist/errors.d.ts +29 -0
  44. package/dist/errors.js +29 -0
  45. package/dist/extensions/admin-ops.d.ts +4 -1
  46. package/dist/extensions/admin-ops.js +22 -1
  47. package/dist/extensions/admin-view.d.ts +8 -0
  48. package/dist/extensions/admin-view.js +78 -0
  49. package/dist/extensions/blobs.d.ts +14 -1
  50. package/dist/extensions/blobs.js +48 -5
  51. package/dist/extensions/host.d.ts +6 -1
  52. package/dist/extensions/host.js +20 -9
  53. package/dist/extensions/package.d.ts +4 -3
  54. package/dist/extensions/package.js +5 -1
  55. package/dist/extensions/permission-set.d.ts +19 -3
  56. package/dist/extensions/permission-set.js +19 -2
  57. package/dist/extensions/permission-view.d.ts +38 -7
  58. package/dist/extensions/permission-view.js +27 -8
  59. package/dist/extensions/sources.d.ts +6 -0
  60. package/dist/extensions/sources.js +2 -0
  61. package/dist/extensions/state.module.js +9 -1
  62. package/dist/http/api.module.js +10 -0
  63. package/dist/identity/settings.d.ts +41 -26
  64. package/dist/identity/settings.js +87 -4
  65. package/dist/infra/throttler/named-throttlers.d.ts +4 -0
  66. package/dist/infra/throttler/named-throttlers.js +14 -0
  67. package/dist/kernel.module.js +3 -0
  68. package/dist/orgs/settings.service.d.ts +2 -26
  69. package/dist/platform/exclusions.js +27 -1
  70. package/dist/platform/ops/admin.ops.js +17 -2
  71. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  72. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  73. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  74. package/dist/platform/ops/org-plugins.ops.js +63 -4
  75. package/dist/platform/ops/org-settings.ops.js +2 -2
  76. package/dist/platform/ops/orgs.ops.js +1 -0
  77. package/dist/platform/ops/ui.ops.js +1 -1
  78. package/dist/platform/platform-ops.module.js +2 -0
  79. package/dist/platform/route-schemas.js +3 -0
  80. package/dist/roles/worker.module.js +4 -0
  81. package/dist/themes/render-css.d.ts +2 -1
  82. package/dist/themes/render-css.js +5 -2
  83. package/dist/themes/theme-delivery.service.d.ts +4 -2
  84. package/dist/themes/theme-delivery.service.js +8 -3
  85. package/dist/themes/theme-state.d.ts +28 -7
  86. package/dist/themes/theme-state.js +24 -6
  87. package/dist/themes/themes-public.controller.d.ts +7 -2
  88. package/dist/themes/themes-public.controller.js +11 -5
  89. package/dist/themes/themes.service.js +2 -2
  90. package/dist/tools/pipeline.d.ts +3 -2
  91. package/dist/ui/arrangement.d.ts +41 -50
  92. package/dist/ui/arrangement.js +88 -15
  93. package/dist/ui/registry.d.ts +13 -3
  94. package/dist/ui/registry.js +15 -4
  95. package/dist/ui/ui.schemas.d.ts +4 -0
  96. package/dist/ui/views.d.ts +36 -9
  97. package/dist/ui/views.js +58 -28
  98. package/package.json +3 -3
  99. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -39,7 +39,7 @@ let OrgSettingsOperations = class OrgSettingsOperations {
39
39
  this.registry.register([
40
40
  defineOperation({
41
41
  id: "org-settings.get",
42
- description: "Read this org's settings: toolSearch, locale, sandboxToolPolicy, platformTool (on, read-only or off), sandboxEgress and wakeOnBackgroundTask (absent means it follows the deployment), theme (the org's default theme id, null or absent following the platform, and personal: allowed or locked), uiArrangement (per UI slot, the extension contributions the org hides and their order; a slot not listed follows the platform), the deployment's sandbox egress default and the sandbox cpu / memory limits in effect.",
42
+ description: "Read this org's settings: toolSearch, locale, sandboxToolPolicy, platformTool (on, read-only or off), sandboxEgress and wakeOnBackgroundTask (absent means it follows the deployment), theme (the org's default theme id, null or absent following the platform, and personal: allowed or locked), uiArrangement (per list slot, the extension contributions the org hides and their order; per single slot, the contribution that replaces the built-in as use, null for the built-in; a slot not listed follows the platform), the deployment's sandbox egress default and the sandbox cpu / memory limits in effect.",
43
43
  input: z.object({}).strict(),
44
44
  example: {},
45
45
  authz: { orgAction: "org.read" },
@@ -53,7 +53,7 @@ let OrgSettingsOperations = class OrgSettingsOperations {
53
53
  }),
54
54
  defineOperation({
55
55
  id: "org-settings.update",
56
- description: "Change this org's settings (org admins). Give only the settings to change; null clears sandboxEgress or wakeOnBackgroundTask so they follow the deployment again. theme: { id, personal } sets the org's default theme (a theme id from themes.list, or null to follow the platform) and whether members may pick their own (allowed) or not (locked); null clears it. uiArrangement: { <slot>: { hidden, order } } replaces that slot's entry, where hidden and order list contribution keys <extension>/<contribution> (hidden ones are not listed in the web UI but their pages still open by URL); null for a slot makes it follow the platform again; platform.page is refused. Setting platformTool to off or read-only takes effect from the next call and cuts this Platform tool off in the org, so say so before asking; an org admin switches it back on in the web settings.",
56
+ description: "Change this org's settings (org admins). Give only the settings to change; null clears sandboxEgress or wakeOnBackgroundTask so they follow the deployment again. theme: { id, personal } sets the org's default theme (a theme id from themes.list, or null to follow the platform) and whether members may pick their own (allowed) or not (locked); null clears it. uiArrangement: { <slot>: { hidden, order } } (a list slot) or { <slot>: { use } } (a single slot: shell.brand, conversation.empty, conversation.composer.frame) replaces that slot's entry, where hidden, order and use name contribution keys <extension>/<contribution> (hidden ones are not listed in the web UI but their pages still open by URL; use null is the built-in); null for a slot makes it follow the platform again; platform.page is refused. Setting platformTool to off or read-only takes effect from the next call and cuts this Platform tool off in the org, so say so before asking; an org admin switches it back on in the web settings.",
57
57
  input: OrgSettingsPatchSchema.refine((i) => Object.values(i).some((v) => v !== undefined), {
58
58
  message: "give at least one setting",
59
59
  }),
@@ -160,6 +160,7 @@ let OrgsOperations = class OrgsOperations {
160
160
  "orgMarketplaceBrowse",
161
161
  "uiTheme",
162
162
  "uiArrangement",
163
+ "clientExtensions",
163
164
  ],
164
165
  extras: ["orgId", "runConcurrency", "sandbox", "subagents"],
165
166
  confirm: "platform",
@@ -22,7 +22,7 @@ let UiOperations = class UiOperations {
22
22
  this.registry.register([
23
23
  defineOperation({
24
24
  id: "ui.slots",
25
- description: "List the UI slots an extension can contribute to (id, version, where it is, scope and args, accepted kinds view / embed, minimum role, anchors). Use a slot id as a ui contribution's `slot` in a plugin manifest.",
25
+ description: "List the UI slots an extension can contribute to (id, version, where it is, scope and args, accepted kinds view / embed / component, cardinality list or single, minimum role, anchors). Use a slot id as a ui contribution's `slot` in a plugin manifest.",
26
26
  input: z.object({}).strict(),
27
27
  example: {},
28
28
  // any signed-in person may call the route; every run's actor is a member of this org
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { Module } from "@nestjs/common";
8
8
  import { AgentsModule } from "../agents/agents.module.js";
9
9
  import { BackgroundModule } from "../background/background.module.js";
10
+ import { AppearanceModule } from "../catalog/appearance.service.js";
10
11
  import { CatalogModule } from "../catalog/catalog.module.js";
11
12
  import { ChannelsModule } from "../channels/channels.module.js";
12
13
  import { ConversationsModule } from "../conversations/conversations.module.js";
@@ -114,6 +115,7 @@ PlatformOpsModule = __decorate([
114
115
  AgentsModule,
115
116
  OrgsModule,
116
117
  CatalogModule,
118
+ AppearanceModule,
117
119
  ExtensionsModule,
118
120
  ToolsModule,
119
121
  ConversationsModule,
@@ -4,6 +4,7 @@ import { AGENT_ROUTES } from "../agents/agents.schemas.js";
4
4
  import { AGENT_TEMPLATE_ROUTES } from "../agents/templates.schemas.js";
5
5
  import { BACKGROUND_ROUTES } from "../background/background.schemas.js";
6
6
  import { ADMIN_CATALOG_ROUTES } from "../catalog/admin-catalog.schemas.js";
7
+ import { APPEARANCE_ROUTES } from "../catalog/appearance.schemas.js";
7
8
  import { COMPONENT_SCOPE_ROUTES } from "../catalog/component-scope.schemas.js";
8
9
  import { MARKETPLACE_ROUTES } from "../catalog/marketplaces.schemas.js";
9
10
  import { ORG_CATALOG_ROUTES } from "../catalog/org-catalog.schemas.js";
@@ -66,6 +67,8 @@ export const ROUTE_SCHEMAS = [
66
67
  ...ME_ROUTES,
67
68
  // themes (themes spec §9)
68
69
  ...THEME_ROUTES,
70
+ // a plugin's suggested appearance (client extensions spec §6.5)
71
+ ...APPEARANCE_ROUTES,
69
72
  // setup guide checks (setup guides v2 §10.3, §12)
70
73
  ...SETUP_CHECK_ROUTES,
71
74
  ...SETUP_GUIDE_ROUTES,
@@ -14,6 +14,8 @@ import { PlatformMigration } from "../catalog/platform-migration.js";
14
14
  import { PluginScopeWorkersModule } from "../catalog/scope-sync-workers.js";
15
15
  import { ChannelsModule, ChannelWorkersModule } from "../channels/channels.module.js";
16
16
  import { GatewayCheckJob } from "../channels/supervisor.js";
17
+ import { ClientModule } from "../client/client.module.js";
18
+ import { ClientReportsCron } from "../client/client-reports.cron.js";
17
19
  import { AttachmentsModule } from "../conversations/attachments.module.js";
18
20
  import { AttachmentCleanupJob } from "../conversations/attachments.service.js";
19
21
  import { EventLogModule } from "../conversations/event-log.js";
@@ -55,6 +57,7 @@ let WorkerModule = WorkerModule_1 = class WorkerModule {
55
57
  module: WorkerModule_1,
56
58
  imports: [
57
59
  KernelScheduleModule,
60
+ ClientModule,
58
61
  // the queue consumers (their worker options come from the startup config)
59
62
  RuntimeWorkersModule.forRoot(config),
60
63
  EvalWorkersModule.forRoot(config),
@@ -87,6 +90,7 @@ let WorkerModule = WorkerModule_1 = class WorkerModule {
87
90
  BackgroundTaskPollJob,
88
91
  ToolJobs,
89
92
  InitialPasswordCleanupJob,
93
+ ClientReportsCron,
90
94
  CatalogJobs,
91
95
  PlatformMigration,
92
96
  DirectoryScanJob,
@@ -7,6 +7,7 @@ import { type ParsedTheme } from "@capybara-hq/plugin-sdk";
7
7
  * the theme's families first, then the built-in stack. Every stored value is re-validated (token
8
8
  * names against the overridable list, values against the SDK grammar, radius 0-16, families,
9
9
  * paths, weights, styles, roles); one that fails is left out, and `onSkipped` (when given and
10
- * something was skipped) is called once with the paths of every skipped value.
10
+ * something was skipped) is called once with the paths of every skipped value. The whole
11
+ * stylesheet is one `@layer theme { … }` block (client extensions spec §7.2).
11
12
  */
12
13
  export declare function renderThemeCss(theme: ParsedTheme, assetUrl: (pluginPath: string) => string, onSkipped?: (paths: string[]) => void): string;
@@ -73,7 +73,8 @@ const declarations = (d, extra = []) => [...extra, ...[...d].map(([k, v]) => `${
73
73
  * the theme's families first, then the built-in stack. Every stored value is re-validated (token
74
74
  * names against the overridable list, values against the SDK grammar, radius 0-16, families,
75
75
  * paths, weights, styles, roles); one that fails is left out, and `onSkipped` (when given and
76
- * something was skipped) is called once with the paths of every skipped value.
76
+ * something was skipped) is called once with the paths of every skipped value. The whole
77
+ * stylesheet is one `@layer theme { … }` block (client extensions spec §7.2).
77
78
  */
78
79
  export function renderThemeCss(theme, assetUrl, onSkipped) {
79
80
  const skipped = [];
@@ -128,5 +129,7 @@ export function renderThemeCss(theme, assetUrl, onSkipped) {
128
129
  rules.push(`:root { ${stacks.join(" ")} }`);
129
130
  if (skipped.length > 0)
130
131
  onSkipped?.(skipped);
131
- return `${rules.join("\n")}\n`;
132
+ // One block in the `theme` cascade layer (client extensions spec §7.2): the design stylesheet
133
+ // declares the order design, theme, extensions; an unlayered theme would beat plugin CSS.
134
+ return `@layer theme {\n${rules.join("\n")}\n}\n`;
132
135
  }
@@ -22,9 +22,11 @@ export declare class ThemeDeliveryService {
22
22
  /**
23
23
  * The stylesheet of version `version` of theme `id` ({@link renderThemeCss}, asset URLs from
24
24
  * {@link themeUrl}); a stored value that fails re-validation is left out with one warning per
25
- * render naming every skipped value. Unknown theme or version: 404 `theme.not_found`.
25
+ * render naming every skipped value. Unknown theme or version, or a `revision` other than the
26
+ * current `r<THEME_CSS_REVISION>` (an older one promised another format; a later one belongs to a
27
+ * newer replica): 404 `theme.not_found`, never cached.
26
28
  */
27
- stylesheet(id: string, version: string): Promise<string>;
29
+ stylesheet(id: string, version: string, revision: string): Promise<string>;
28
30
  /**
29
31
  * Asset `file` (a path inside the plugin, as stored: `fonts/MiSans.woff2`) of version `version`
30
32
  * of theme `id`: only a font or logo the version's definition names. Unknown theme, version or
@@ -15,7 +15,7 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
15
15
  import { packagePath } from "../packages/tgz.js";
16
16
  import { StorageService } from "../storage/storage.service.js";
17
17
  import { renderThemeCss } from "./render-css.js";
18
- import { themeUrl } from "./theme-state.js";
18
+ import { THEME_CSS_REVISION, themeUrl } from "./theme-state.js";
19
19
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
20
  /** A version number as it appears in a delivery URL: a positive int4. */
21
21
  const VERSION_RE = /^[1-9]\d{0,9}$/;
@@ -61,9 +61,14 @@ let ThemeDeliveryService = class ThemeDeliveryService {
61
61
  /**
62
62
  * The stylesheet of version `version` of theme `id` ({@link renderThemeCss}, asset URLs from
63
63
  * {@link themeUrl}); a stored value that fails re-validation is left out with one warning per
64
- * render naming every skipped value. Unknown theme or version: 404 `theme.not_found`.
64
+ * render naming every skipped value. Unknown theme or version, or a `revision` other than the
65
+ * current `r<THEME_CSS_REVISION>` (an older one promised another format; a later one belongs to a
66
+ * newer replica): 404 `theme.not_found`, never cached.
65
67
  */
66
- async stylesheet(id, version) {
68
+ async stylesheet(id, version, revision) {
69
+ if (revision !== `r${THEME_CSS_REVISION}`) {
70
+ throw notFound(`Theme ${id} version ${version} stylesheet revision ${revision}`);
71
+ }
67
72
  const { definition } = await this.version(id, version);
68
73
  return renderThemeCss(definition, (path) => themeUrl(id, version, path), (skipped) => this.log.warn("theme stylesheet: stored values failed re-validation and were left out", {
69
74
  themeId: id,
@@ -6,6 +6,11 @@ import { ApiError } from "../errors.js";
6
6
  export interface ThemeRef {
7
7
  /** The `platform_themes` id (what settings and picks store). */
8
8
  id: string;
9
+ /**
10
+ * `"<plugin>/<theme file id>"`: the name the web's `theme=` state token and `useTheme()` carry
11
+ * (client extensions spec §7.3). Stable across versions, unlike `id`.
12
+ */
13
+ key: string;
9
14
  /** Its current version. */
10
15
  version: string;
11
16
  name: LocalizedName;
@@ -19,7 +24,7 @@ export interface ThemeRef {
19
24
  wordmark: string | null;
20
25
  faviconUrl: string | null;
21
26
  } | null;
22
- /** `/api/v1/themes/<id>/<version>/theme.css`. */
27
+ /** `/api/v1/themes/<id>/<version>/r<THEME_CSS_REVISION>/theme.css`. */
23
28
  cssUrl: string;
24
29
  }
25
30
  /** The four colors a theme picker draws per scheme. */
@@ -34,24 +39,40 @@ export type ListedTheme = ThemeRef & {
34
39
  swatches: Partial<Record<ThemeScheme, ThemeSwatch>>;
35
40
  };
36
41
  /**
37
- * The delivery URL of a theme version (spec §8): its stylesheet, or with `asset` (a path inside the
42
+ * The render revision of theme stylesheets, a segment of the stylesheet URL (`r<N>/theme.css`).
43
+ * The stylesheet is cached as immutable for a year, keyed by its URL: bump this whenever
44
+ * `renderThemeCss`'s output format changes, so no browser keeps an older format under a URL
45
+ * that now means the new one. The delivery route serves only the current revision (404
46
+ * `theme.not_found` otherwise). History: (none, `<version>/theme.css`) plain rules; 2: one
47
+ * `@layer theme { … }` block (client extensions spec §7.2).
48
+ */
49
+ export declare const THEME_CSS_REVISION = 2;
50
+ /**
51
+ * The delivery URL of a theme version (spec §8): its stylesheet (with the render revision,
52
+ * {@link THEME_CSS_REVISION}), or with `asset` (a path inside the
38
53
  * plugin as the theme file names it, e.g. `./fonts/MiSans.woff2`) that asset, as one path segment
39
54
  * (`assets/fonts%2FMiSans.woff2`; the route decodes it back to the stored path `fonts/MiSans.woff2`).
40
55
  * The one builder of these URLs: the listing, the stylesheet and the asset routes all use it.
41
56
  */
42
57
  export declare function themeUrl(id: string, version: string, asset?: string): string;
43
- /** The {@link ThemeRef} of version `version` of theme `id`. */
44
- export declare function themeRefOf(id: string, version: string, def: ParsedTheme): ThemeRef;
58
+ /**
59
+ * The {@link ThemeRef} of version `version` of theme `id` of plugin `plugin` (null only for a row
60
+ * without provenance: the key is then the file id alone).
61
+ */
62
+ export declare function themeRefOf(id: string, version: string, def: ParsedTheme, plugin: string | null): ThemeRef;
45
63
  /**
46
64
  * The swatches of each scheme the theme defines: its overrides merged onto the current built-in
47
65
  * values (a stored definition holds only the theme file's own values, themes spec §6).
48
66
  */
49
67
  export declare function themeSwatches(def: ParsedTheme): Partial<Record<ThemeScheme, ThemeSwatch>>;
50
- /** The current definitions of live themes `ids`, by id. */
51
- export declare function themeDefinitions(db: KernelSchemaDb, ids: readonly string[]): Promise<Map<string, {
68
+ /** A live theme's current definition and its plugin (null: no provenance). */
69
+ export interface ThemeDefinition {
52
70
  version: string;
53
71
  definition: ParsedTheme;
54
- }>>;
72
+ plugin: string | null;
73
+ }
74
+ /** The current definitions of live themes `ids`, by id. */
75
+ export declare function themeDefinitions(db: KernelSchemaDb, ids: readonly string[]): Promise<Map<string, ThemeDefinition>>;
55
76
  /** The live themes on in the org, sorted by id. */
56
77
  export declare function themesOnIn(db: KernelSchemaDb, orgId: string): Promise<ComponentRow[]>;
57
78
  /** The live theme `id` if it can be the platform default, else null. */
@@ -9,7 +9,17 @@ import { packagePath } from "../packages/tgz.js";
9
9
  /** The schemes in the order they are listed. */
10
10
  const SCHEMES = ["dark", "light"];
11
11
  /**
12
- * The delivery URL of a theme version (spec §8): its stylesheet, or with `asset` (a path inside the
12
+ * The render revision of theme stylesheets, a segment of the stylesheet URL (`r<N>/theme.css`).
13
+ * The stylesheet is cached as immutable for a year, keyed by its URL: bump this whenever
14
+ * `renderThemeCss`'s output format changes, so no browser keeps an older format under a URL
15
+ * that now means the new one. The delivery route serves only the current revision (404
16
+ * `theme.not_found` otherwise). History: (none, `<version>/theme.css`) plain rules; 2: one
17
+ * `@layer theme { … }` block (client extensions spec §7.2).
18
+ */
19
+ export const THEME_CSS_REVISION = 2;
20
+ /**
21
+ * The delivery URL of a theme version (spec §8): its stylesheet (with the render revision,
22
+ * {@link THEME_CSS_REVISION}), or with `asset` (a path inside the
13
23
  * plugin as the theme file names it, e.g. `./fonts/MiSans.woff2`) that asset, as one path segment
14
24
  * (`assets/fonts%2FMiSans.woff2`; the route decodes it back to the stored path `fonts/MiSans.woff2`).
15
25
  * The one builder of these URLs: the listing, the stylesheet and the asset routes all use it.
@@ -17,14 +27,18 @@ const SCHEMES = ["dark", "light"];
17
27
  export function themeUrl(id, version, asset) {
18
28
  const base = `/api/v1/themes/${encodeURIComponent(id)}/${encodeURIComponent(version)}`;
19
29
  return asset === undefined
20
- ? `${base}/theme.css`
30
+ ? `${base}/r${THEME_CSS_REVISION}/theme.css`
21
31
  : `${base}/assets/${encodeURIComponent(packagePath(asset))}`;
22
32
  }
23
- /** The {@link ThemeRef} of version `version` of theme `id`. */
24
- export function themeRefOf(id, version, def) {
33
+ /**
34
+ * The {@link ThemeRef} of version `version` of theme `id` of plugin `plugin` (null only for a row
35
+ * without provenance: the key is then the file id alone).
36
+ */
37
+ export function themeRefOf(id, version, def, plugin) {
25
38
  const url = (ref) => (ref === undefined ? null : themeUrl(id, version, ref));
26
39
  return {
27
40
  id,
41
+ key: plugin === null ? def.id : `${plugin}/${def.id}`,
28
42
  version,
29
43
  name: def.name,
30
44
  schemes: SCHEMES.filter((s) => def.schemes[s] !== undefined),
@@ -62,12 +76,16 @@ export async function themeDefinitions(db, ids) {
62
76
  const rows = await db
63
77
  .selectFrom("platform_themes as p")
64
78
  .innerJoin("platform_theme_versions as v", (j) => j.onRef("v.theme_id", "=", "p.id").onRef("v.version", "=", "p.current_version"))
65
- .select(["p.id", "p.current_version", "v.definition"])
79
+ .select(["p.id", "p.current_version", "p.plugin_name", "v.definition"])
66
80
  .where("p.id", "in", [...ids])
67
81
  .where("p.deleted_at", "is", null)
68
82
  .execute();
69
83
  for (const r of rows) {
70
- out.set(r.id, { version: String(r.current_version), definition: r.definition });
84
+ out.set(r.id, {
85
+ version: String(r.current_version),
86
+ definition: r.definition,
87
+ plugin: r.plugin_name,
88
+ });
71
89
  }
72
90
  return out;
73
91
  }
@@ -10,8 +10,13 @@ import { ThemesService } from "./themes.service.js";
10
10
  export declare class ThemeDeliveryController {
11
11
  private readonly delivery;
12
12
  constructor(delivery: ThemeDeliveryService);
13
- /** The version's stylesheet: scheme overrides, `@font-face` rules and font stacks. */
14
- css(id: string, version: string, reply: FastifyReply): Promise<void>;
13
+ /**
14
+ * The version's stylesheet: scheme overrides, `@font-face` rules and font stacks, in the render
15
+ * revision `revision` (`r<THEME_CSS_REVISION>`; only the current one is served). The URL without
16
+ * a revision (before revision 2) has no route: 404, so a browser's cached unlayered copy is never
17
+ * answered with the new format under an immutable cache.
18
+ */
19
+ css(id: string, version: string, revision: string, reply: FastifyReply): Promise<void>;
15
20
  /**
16
21
  * A font (`font/woff2`) or logo (`image/svg+xml`, sanitized at import, served under a CSP
17
22
  * sandbox; `image/png`) the version names; `file` is its plugin path as one segment
@@ -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[];