@k2b/cloud 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/package.json +8 -3
  2. package/scripts/build-canvas-workers.ts +109 -29
  3. package/scripts/build.ts +3 -5
  4. package/src/_internal/app-path.ts +25 -0
  5. package/src/_internal/app-presentation.ts +4 -0
  6. package/src/_internal/canvas-worker.ts +5 -3
  7. package/src/_internal/define-app.ts +28 -2
  8. package/src/_internal/help-catalog.ts +123 -4
  9. package/src/_internal/help.ts +27 -6
  10. package/src/_internal/panel-cookie.ts +39 -0
  11. package/src/_internal/registry-validation.ts +21 -26
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  14. package/src/access/PermissionEditor.tsx +30 -5
  15. package/src/access/ResourceApiKeys.tsx +5 -1
  16. package/src/account/Avatar.tsx +20 -2
  17. package/src/account/ui.ts +1 -1
  18. package/src/ai/approval-routes.ts +5 -2
  19. package/src/ai/approvals.ts +139 -28
  20. package/src/ai/browser.ts +11 -0
  21. package/src/ai/chart-block.ts +44 -13
  22. package/src/ai/chat/action-sentences.ts +1 -44
  23. package/src/ai/chat/chart-block.tsx +2 -11
  24. package/src/ai/chat/messages.ts +10 -0
  25. package/src/ai/chat/turn-view.tsx +35 -3
  26. package/src/ai/chat/view-block.tsx +7 -2
  27. package/src/ai/chat/web-tools.tsx +23 -6
  28. package/src/ai/chat-task-contracts.ts +28 -3
  29. package/src/ai/chat-task-reasons-migrate.ts +68 -0
  30. package/src/ai/chat-task-reasons.ts +92 -0
  31. package/src/ai/chat-tasks.ts +460 -66
  32. package/src/ai/default-tool-contracts.ts +30 -13
  33. package/src/ai/fetch-file-tool.ts +37 -3
  34. package/src/ai/index.ts +12 -0
  35. package/src/ai/migrate.ts +46 -3
  36. package/src/ai/readable-html.ts +1057 -0
  37. package/src/ai/request-context.ts +7 -1
  38. package/src/ai/routes.ts +27 -1
  39. package/src/ai/runtime.ts +12 -27
  40. package/src/ai/store.ts +5 -3
  41. package/src/ai/timeline.ts +9 -1
  42. package/src/ai/tool-wording.ts +45 -0
  43. package/src/ai/turn-abort.ts +33 -0
  44. package/src/ai/types.ts +12 -1
  45. package/src/api/admin-core-settings.ts +190 -139
  46. package/src/api/admin-outgoing-mail.ts +4 -2
  47. package/src/api/app-approval.ts +9 -2
  48. package/src/api/approvals.ts +1 -1
  49. package/src/api/help.ts +23 -4
  50. package/src/api/index.ts +2 -1
  51. package/src/api/mcp.ts +8 -2
  52. package/src/browser/CloudResourceSearch.tsx +10 -6
  53. package/src/browser/FileChooser.tsx +11 -3
  54. package/src/browser/app-approval.ts +7 -2
  55. package/src/browser/choose-files.browser-harness.tsx +4 -1
  56. package/src/browser/composer-catalog.ts +103 -0
  57. package/src/browser/composer-insert-messages.ts +31 -0
  58. package/src/browser/composer-insert.ts +150 -0
  59. package/src/browser/file-chooser-messages.ts +4 -0
  60. package/src/browser/file-providers.ts +14 -1
  61. package/src/browser/panel-messages.ts +24 -0
  62. package/src/browser/panel-slot.ts +354 -0
  63. package/src/browser/panel.ts +28 -0
  64. package/src/browser/provider-folder.tsx +58 -5
  65. package/src/browser/resource-picker.tsx +17 -2
  66. package/src/cli/admin/workflows.ts +1 -0
  67. package/src/config/env.ts +14 -0
  68. package/src/contracts/app-approval.ts +49 -9
  69. package/src/contracts/app.ts +14 -0
  70. package/src/contracts/file-provider.ts +48 -1
  71. package/src/contracts/outgoing-mail.ts +8 -1
  72. package/src/contracts/registry.ts +3 -1
  73. package/src/contracts/shared.ts +2 -0
  74. package/src/server/help.ts +136 -10
  75. package/src/server/services/access.ts +5 -1
  76. package/src/server/services/geo.ts +21 -4
  77. package/src/services/accounts/posix.ts +2 -1
  78. package/src/services/allowed-network-fetch.ts +189 -0
  79. package/src/services/app-approval.ts +158 -38
  80. package/src/services/approvals/device.ts +2 -0
  81. package/src/services/help/index.ts +46 -5
  82. package/src/services/help/store.ts +26 -5
  83. package/src/services/help/types.ts +10 -2
  84. package/src/services/index.ts +22 -3
  85. package/src/services/notifications/browser.ts +16 -5
  86. package/src/services/outgoing-mail/dispatcher.ts +7 -2
  87. package/src/services/outgoing-mail/resolve-host.ts +39 -0
  88. package/src/services/outgoing-mail/test-send.ts +18 -3
  89. package/src/services/outgoing-mail/transport.ts +24 -13
  90. package/src/services/pdf/image-protocol.ts +135 -0
  91. package/src/services/pdf/image-variants.ts +124 -0
  92. package/src/services/pdf/image-worker.ts +31 -0
  93. package/src/services/pdf/image.ts +109 -0
  94. package/src/services/pdf/index.ts +9 -0
  95. package/src/services/settings/api.ts +11 -6
  96. package/src/services/settings/app.ts +5 -4
  97. package/src/services/settings/core-settings.ts +4 -3
  98. package/src/services/settings/index.ts +67 -43
  99. package/src/services/settings/namespace.ts +2 -1
  100. package/src/services/settings/store.ts +83 -11
  101. package/src/shared/ai-platform-prompt.ts +1 -1
  102. package/src/shared/app-presentation.ts +1 -0
  103. package/src/shared/help.ts +41 -0
  104. package/src/shared/index.ts +9 -1
  105. package/src/shared/markdown/extensions/guided-help.ts +208 -64
  106. package/src/shared/markdown/formula.ts +8 -8
  107. package/src/shared/markdown/index.ts +72 -11
  108. package/src/shared/redirect.ts +12 -0
  109. package/src/ssr/CloudShell.island.tsx +86 -9
  110. package/src/ssr/Layout.tsx +62 -11
  111. package/src/ssr/LayoutHeader.tsx +15 -9
  112. package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
  113. package/src/ssr/LayoutHelp.tsx +569 -410
  114. package/src/ssr/LayoutRail.tsx +2 -2
  115. package/src/ssr/LayoutTabBar.tsx +29 -0
  116. package/src/ssr/PanelLayout.tsx +27 -0
  117. package/src/ssr/PanelRuntime.island.tsx +87 -0
  118. package/src/ssr/PanelSlot.tsx +108 -0
  119. package/src/ssr/PanelTrigger.tsx +49 -0
  120. package/src/ssr/ShellRuntime.island.tsx +72 -25
  121. package/src/ssr/app-appearance.ts +3 -2
  122. package/src/ssr/app-color.ts +100 -0
  123. package/src/ssr/app-launchpad.tsx +18 -1
  124. package/src/ssr/app-navigation.ts +15 -0
  125. package/src/ssr/compact-shell.baseline.json +5270 -0
  126. package/src/ssr/compact-shell.browser-harness.ts +431 -0
  127. package/src/ssr/help-messages.ts +60 -38
  128. package/src/ssr/help.ts +3 -0
  129. package/src/ssr/index.ts +3 -0
  130. package/src/ssr/layout-help-article.ts +41 -0
  131. package/src/ssr/layout-help-navigation.ts +0 -12
  132. package/src/ssr/layout-help-registry.tsx +4 -11
  133. package/src/ssr/layout-help-search.ts +9 -12
  134. package/src/ssr/layout-help-start.ts +68 -0
  135. package/src/ssr/panel-slot.browser-harness.tsx +110 -0
  136. package/src/ssr/platform-messages.ts +34 -0
  137. package/src/ssr/rail-navigation.ts +44 -0
  138. package/src/ssr/shell-data.tsx +11 -0
  139. package/src/ssr/shell-shortcuts.ts +19 -0
  140. package/src/styles/effects.css +689 -185
  141. package/src/styles/global.css +269 -55
  142. package/src/styles/panel.css +373 -0
  143. package/src/styles/resource-search.css +25 -0
  144. package/src/styles/tokens.css +23 -6
  145. package/src/styles/utilities-navigation.css +5 -21
  146. package/src/workflows/store/events.ts +24 -0
  147. package/src/workflows/store/index.ts +8 -0
  148. package/src/workflows/store/observability.ts +6 -1
  149. package/src/workflows/store/retention.ts +157 -0
@@ -3,7 +3,8 @@ import { chooseFiles } from "./files";
3
3
 
4
4
  /**
5
5
  * One "Attach" button that calls the real `chooseFiles` for a browser test, which answers the catalog, the
6
- * provider's Queries, and its read streams. `?single` chooses one file, `?accept=` and `?max=` pass through.
6
+ * provider's Queries, and its read streams. `?single` chooses one file, `?accept=` and `?max=` pass through,
7
+ * `?customer=` passes one `customers.customer` ref as context and `?noMatching=` its empty title.
7
8
  * Each call gets a fresh signal that `window.abortChoosing()` aborts. Each result lands in `window.chosen` as plain
8
9
  * facts. `?dropzone` opens, like Notebooks, a dialog whose `FileDropzone` chooses with
9
10
  * `chooseFiles`; what it hands over lands in `window.chosen` the same way.
@@ -43,6 +44,8 @@ button.addEventListener("click", () => {
43
44
  multiple: !params.has("single"),
44
45
  accept: params.get("accept") ?? undefined,
45
46
  maxBytes: params.has("max") ? Number(params.get("max")) : undefined,
47
+ context: params.has("customer") ? { refs: [{ type: "customers.customer", id: params.get("customer")! }] } : undefined,
48
+ noMatchingFiles: params.get("noMatching") ?? undefined,
46
49
  signal: choosing.signal,
47
50
  }).then(async (files) => void results.push(await facts(files)));
48
51
  });
@@ -0,0 +1,103 @@
1
+ import type { ChatCommandSearch } from "@k2b/ui";
2
+ import { createSignal } from "solid-js";
3
+ import { listCapabilityCatalog } from "../capabilities/client";
4
+ import { LOCALE_HEADER } from "../shared/locale";
5
+
6
+ export type CloudSearchTag = {
7
+ appId: string;
8
+ appName: string;
9
+ appIcon: string;
10
+ tag: string;
11
+ title: string;
12
+ description: string;
13
+ aliases: readonly string[];
14
+ };
15
+
16
+ /**
17
+ * What composers read from the page's one Capability catalog load. It leaves out the tags of apps without a resource
18
+ * reader: a picker that requires one can show none of their results. A composer that reads such a type itself adds its
19
+ * tag on its own and names the type in `cloudInsertMenu`'s `ownReaders`.
20
+ */
21
+ export type CloudComposerCatalog = { tags: readonly CloudSearchTag[] };
22
+
23
+ /** The budget of one Capability query (and of resolving a Command), applied to each catalog page. */
24
+ const CATALOG_PAGE_TIMEOUT_MS = 15_000;
25
+
26
+ const catalogs = new Map<string, Promise<CloudComposerCatalog>>();
27
+
28
+ const loadCatalog = async (locale: string): Promise<CloudComposerCatalog> => {
29
+ const tags: CloudSearchTag[] = [];
30
+ const seen = new Set<string>();
31
+ let cursor: string | undefined;
32
+ do {
33
+ // A page that never answers fails like any other, so the load is forgotten and the next opening asks again.
34
+ const result = await listCapabilityCatalog({
35
+ limit: 25,
36
+ cursor,
37
+ headers: { [LOCALE_HEADER]: locale },
38
+ signal: AbortSignal.timeout(CATALOG_PAGE_TIMEOUT_MS),
39
+ });
40
+ if (!result.ok) throw new Error(result.error.message);
41
+ for (const app of result.data.apps) {
42
+ if (!app.manifest.types.some((type) => type.reader)) continue;
43
+ for (const query of app.manifest.queries) {
44
+ for (const tag of query.universalSearch?.tags ?? []) {
45
+ const key = `${app.appId}\u0000${tag.tag}`;
46
+ if (seen.has(key)) continue;
47
+ seen.add(key);
48
+ tags.push({ appId: app.appId, appName: app.appName, appIcon: app.appIcon, ...tag, aliases: tag.aliases ?? [] });
49
+ }
50
+ }
51
+ }
52
+ const next = result.data.page.hasMore ? result.data.page.nextCursor : undefined;
53
+ if (next && cursor && next <= cursor) throw new Error("Invalid Composer catalog cursor");
54
+ cursor = next;
55
+ } while (cursor);
56
+ return { tags };
57
+ };
58
+
59
+ /**
60
+ * One catalog load per page and locale, shared by every composer on the page. A failed load is forgotten,
61
+ * so the next menu opening can try again; nothing polls or retries on its own.
62
+ */
63
+ export const loadCloudComposerCatalog = (locale: string): Promise<CloudComposerCatalog> => {
64
+ const cached = catalogs.get(locale);
65
+ if (cached) return cached;
66
+ const pending = loadCatalog(locale).catch((error: unknown) => {
67
+ if (catalogs.get(locale) === pending) catalogs.delete(locale);
68
+ throw error;
69
+ });
70
+ catalogs.set(locale, pending);
71
+ return pending;
72
+ };
73
+
74
+ /**
75
+ * The search tags of one composer. Call `ready` at the start of every `searchCommands` run: each menu opening starts
76
+ * the shared load once, so only the first opening, or one after a failure, sends a request, and no keystroke does.
77
+ * A search with typed text and no scope waits for that answer, so "/notiz" never settles as "nothing found" before its
78
+ * tag arrives. The overview and scoped searches do not wait, and after a failed load no search waits again; a later
79
+ * success still brings the tags in. A failure keeps the tags the composer had.
80
+ */
81
+ export function createCloudComposerCatalog(load: (locale: string) => Promise<CloudComposerCatalog> = loadCloudComposerCatalog) {
82
+ const [tags, setTags] = createSignal<readonly CloudSearchTag[]>([]);
83
+ let opening: number | undefined;
84
+ let settled: Promise<void> = Promise.resolve();
85
+ let failed = false;
86
+ return {
87
+ tags,
88
+ ready(query: string, search: ChatCommandSearch, locale: string): Promise<void> {
89
+ if (opening !== search.opening) {
90
+ opening = search.opening;
91
+ settled = load(locale).then(
92
+ (catalog) => {
93
+ setTags(catalog.tags);
94
+ },
95
+ () => {
96
+ failed = true;
97
+ },
98
+ );
99
+ }
100
+ return failed || search.scope !== undefined || !query.trim() ? Promise.resolve() : settled;
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,31 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ export const composerInsertMessages = i18n.define({
4
+ baseLocale: "en",
5
+ messages: {
6
+ en: {
7
+ insertFromCloud: "Insert from Cloud",
8
+ insertFromCloudHint: "Find something in your apps and insert it",
9
+ insertTag: ({ tag }: { tag: string }) => `Insert ${tag}`,
10
+ chooseFrom: ({ app }: { app: string }) => `Choose from ${app}`,
11
+ tagFromApp: ({ tag, app }: { tag: string; app: string }) => `${tag} from ${app}`,
12
+ searchTagFor: ({ tag, query }: { tag: string; query: string }) => `Search ${tag} for “${query}”`,
13
+ searchTag: ({ tag }: { tag: string }) => `Search ${tag}`,
14
+ fromAppWithPreview: ({ app }: { app: string }) => `From ${app}, with preview`,
15
+ searchCloudFor: ({ query }: { query: string }) => `Search Cloud for “${query}”`,
16
+ searchAllApps: "Search all apps for content",
17
+ },
18
+ de: {
19
+ insertFromCloud: "Aus Cloud einfügen",
20
+ insertFromCloudHint: "Etwas aus deinen Apps suchen und einfügen",
21
+ insertTag: ({ tag }) => `${tag} einfügen`,
22
+ chooseFrom: ({ app }) => `Aus ${app} wählen`,
23
+ tagFromApp: ({ tag, app }) => `${tag} aus ${app}`,
24
+ searchTagFor: ({ tag, query }) => `„${query}“ in ${tag} suchen`,
25
+ searchTag: ({ tag }) => `${tag} durchsuchen`,
26
+ fromAppWithPreview: ({ app }) => `Aus ${app}, mit Vorschau`,
27
+ searchCloudFor: ({ query }) => `„${query}“ in Cloud suchen`,
28
+ searchAllApps: "In allen Apps nach Inhalten suchen",
29
+ },
30
+ },
31
+ });
@@ -0,0 +1,150 @@
1
+ import type { ChatAttachment, ChatCommand, ChatCommandResult, ChatCommandScope } from "@k2b/ui";
2
+ import type { CloudResourceRef } from "../contracts";
3
+ import type { CloudSearchTag } from "./composer-catalog";
4
+ import { composerInsertMessages } from "./composer-insert-messages";
5
+ import { type CloudResourcePickerItem, openCloudResourcePicker } from "./resource-picker";
6
+
7
+ export {
8
+ type CloudComposerCatalog,
9
+ type CloudSearchTag,
10
+ createCloudComposerCatalog,
11
+ loadCloudComposerCatalog,
12
+ } from "./composer-catalog";
13
+
14
+ export type CloudInsertMenuOptions = {
15
+ locale: string;
16
+ /** Every app's search tags, from `loadCloudComposerCatalog`. */
17
+ tags: readonly CloudSearchTag[];
18
+ /** Section of the rows in the composer's slash list. */
19
+ section?: string;
20
+ /** Turns the chosen resource into the reference the composer puts where `/…` was. */
21
+ mention: (item: CloudResourcePickerItem) => ChatAttachment;
22
+ /** References the draft already holds; the picker leaves them out. Read when the picker opens. */
23
+ excludeRefs?: () => readonly CloudResourceRef[];
24
+ /** Why nothing can be inserted now, such as a full message; it replaces each row's description. */
25
+ reason?: string;
26
+ /** First words of the composer's own scopes. A tag never claims them, so they keep narrowing as before. */
27
+ reservedWords?: readonly string[];
28
+ /** More first words for one tag, keyed `appId:tag`, such as a short word an app uses for its own tag. */
29
+ scopeWords?: Readonly<Record<string, readonly string[]>>;
30
+ /**
31
+ * Tags, keyed `appId:tag`, whose resources of one type this composer reads itself, such as
32
+ * `{ "assistant:studio-app": "assistant.app" }`. Their picker lists only that type, also without a Cloud reader, even
33
+ * after the person removes the scope chip. Every other picker lists only resources with a reader (`requireReader`).
34
+ */
35
+ ownReaders?: Readonly<Record<string, string>>;
36
+ };
37
+
38
+ export type CloudInsertMenu = {
39
+ /** "Insert from Cloud": the picker across all apps. */
40
+ insert: ChatCommand;
41
+ /** One row per search tag, only while searching. Enter opens the picker narrowed to the tag; Tab narrows the list. */
42
+ tags: ChatCommand[];
43
+ /** One scope per tag that keeps a first word of its own, titled like "Notes from Notebooks". */
44
+ scopes: ChatCommandScope[];
45
+ /** For the composer's `commandFallback`: "Search Notes for “q3”" in a tag scope, "Search Cloud for “q3”" otherwise. */
46
+ fallback: (search: { query: string; scope?: string }) => ChatCommand | undefined;
47
+ };
48
+
49
+ const fold = (text: string) => text.toLowerCase().normalize("NFD").replace(/\p{M}/gu, "").replace(/ß/g, "ss");
50
+ const tagKey = (tag: Pick<CloudSearchTag, "appId" | "tag">) => `${tag.appId}:${tag.tag}`;
51
+ const scopeId = (tag: Pick<CloudSearchTag, "appId" | "tag">) => `cloud-tag:${tagKey(tag)}`;
52
+
53
+ /**
54
+ * Slash-list rows that insert Cloud content the person can read. Every row opens the resource picker, narrowed on the
55
+ * server to an app and tag when it has one, and hands the chosen item back as a `mention` for the token's place.
56
+ */
57
+ export const cloudInsertMenu = (options: CloudInsertMenuOptions): CloudInsertMenu => {
58
+ const t = composerInsertMessages.resolve([options.locale]).t;
59
+ const pick = async (tag: CloudSearchTag | undefined, query: string): Promise<ChatCommandResult> => {
60
+ const ownType = tag ? options.ownReaders?.[tagKey(tag)] : undefined;
61
+ const item = await openCloudResourcePicker({
62
+ title: tag ? t.insertTag({ tag: tag.title }) : t.insertFromCloud,
63
+ scope: tag ? { appId: tag.appId, tag: tag.tag, label: `${tag.title} · ${tag.appName}`, icon: tag.appIcon } : undefined,
64
+ query: query || undefined,
65
+ ...(ownType ? { resourceType: ownType } : { requireReader: true }),
66
+ excludeRefs: options.excludeRefs?.(),
67
+ });
68
+ return item ? { mention: options.mention(item) } : "cancelled";
69
+ };
70
+
71
+ const reserved = new Set((options.reservedWords ?? []).map(fold));
72
+ // The title comes first, so Tab writes the word the person reads ("notizen ").
73
+ const candidates = options.tags.map((tag) => {
74
+ const title = fold(tag.title);
75
+ return [
76
+ ...new Set(
77
+ [...(/\s/.test(title) ? [] : [title]), tag.tag, ...tag.aliases, ...(options.scopeWords?.[tagKey(tag)] ?? [])]
78
+ .map(fold)
79
+ .filter((word) => word && !/\s/.test(word) && !reserved.has(word)),
80
+ ),
81
+ ];
82
+ });
83
+ // A word two tags share narrows to neither, so Tab on either row would leave the list unscoped; neither keeps it.
84
+ const uses = new Map<string, number>();
85
+ for (const word of candidates.flat()) uses.set(word, (uses.get(word) ?? 0) + 1);
86
+ const scoped = new Map<string, CloudSearchTag>();
87
+ const scopes: ChatCommandScope[] = [];
88
+ const tags = options.tags.map((tag, index): ChatCommand => {
89
+ const words = candidates[index]!.filter((word) => uses.get(word) === 1);
90
+ if (words.length) {
91
+ scoped.set(scopeId(tag), tag);
92
+ scopes.push({ id: scopeId(tag), title: t.tagFromApp({ tag: tag.title, app: tag.appName }), words });
93
+ }
94
+ return {
95
+ id: scopeId(tag),
96
+ section: options.section,
97
+ name: tag.tag,
98
+ label: tag.title,
99
+ description: t.chooseFrom({ app: tag.appName }),
100
+ icon: tag.appIcon,
101
+ keywords: [tag.title, tag.appName, tag.tag, ...tag.aliases, ...(options.scopeWords?.[tagKey(tag)] ?? [])],
102
+ app: tag.appName,
103
+ scope: words.length ? scopeId(tag) : undefined,
104
+ hiddenUntilSearch: true,
105
+ reason: options.reason,
106
+ action: () => pick(tag, ""),
107
+ };
108
+ });
109
+
110
+ return {
111
+ insert: {
112
+ id: "cloud-insert",
113
+ section: options.section,
114
+ name: "cloud",
115
+ label: t.insertFromCloud,
116
+ description: t.insertFromCloudHint,
117
+ icon: "ti ti-cloud-search",
118
+ keywords: ["cloud", "insert", "einfügen", "inhalt", "content", "ressource", "resource", "verweis", "reference", "link"],
119
+ reason: options.reason,
120
+ action: () => pick(undefined, ""),
121
+ },
122
+ tags,
123
+ scopes,
124
+ fallback: ({ query, scope }) => {
125
+ if (scope !== undefined) {
126
+ const tag = scoped.get(scope);
127
+ if (!tag) return undefined;
128
+ return {
129
+ id: `cloud-search:${tagKey(tag)}`,
130
+ name: `cloud-search:${tagKey(tag)}`,
131
+ label: query ? t.searchTagFor({ tag: tag.title, query }) : t.searchTag({ tag: tag.title }),
132
+ description: t.fromAppWithPreview({ app: tag.appName }),
133
+ icon: "ti ti-search",
134
+ reason: options.reason,
135
+ action: () => pick(tag, query),
136
+ };
137
+ }
138
+ if (!query) return undefined;
139
+ return {
140
+ id: "cloud-search",
141
+ name: "cloud-search",
142
+ label: t.searchCloudFor({ query }),
143
+ description: t.searchAllApps,
144
+ icon: "ti ti-search",
145
+ reason: options.reason,
146
+ action: () => pick(undefined, query),
147
+ };
148
+ },
149
+ };
150
+ };
@@ -19,6 +19,8 @@ export const fileChooserMessages = i18n.define({
19
19
  loading: "Loading…",
20
20
  loadMore: "Load more",
21
21
  emptyFolder: "This folder is empty",
22
+ noMatchingFiles: "No matching files",
23
+ otherSource: "Choose another source",
22
24
  noMatches: ({ query }: { query: string }) => `Nothing matches “${query}”`,
23
25
  clearFilter: "Clear filter",
24
26
  offline: "You are offline",
@@ -92,6 +94,8 @@ export const fileChooserMessages = i18n.define({
92
94
  loading: "Wird geladen…",
93
95
  loadMore: "Mehr laden",
94
96
  emptyFolder: "Dieser Ordner ist leer",
97
+ noMatchingFiles: "Keine passenden Dateien",
98
+ otherSource: "Andere Quelle wählen",
95
99
  noMatches: ({ query }) => `Nichts passt zu „${query}“`,
96
100
  clearFilter: "Filter löschen",
97
101
  offline: "Du bist offline",
@@ -6,12 +6,14 @@ import type { CapabilityCatalogApp, CapabilityClientError, CapabilityHttpOptions
6
6
  import { CapabilityErrorSchema, type CapabilityStream, capabilityResultSchema } from "../contracts/capabilities";
7
7
  import { CapabilityStreamStatusSchema } from "../contracts/capability-streams";
8
8
  import {
9
+ type FileProviderContext,
9
10
  type FileProviderEntry,
10
11
  type FileProviderIssue,
11
12
  FileProviderListDataSchema,
12
13
  FileProviderReadDataSchema,
13
14
  FileProviderSaveDataSchema,
14
15
  FileProviderSaveInputSchema,
16
+ fileProviderAcceptsContext,
15
17
  fileProviderIssues,
16
18
  } from "../contracts/file-provider";
17
19
  import { LOCALE_HEADER } from "../shared/locale";
@@ -25,6 +27,7 @@ export type FileProviderSource = {
25
27
  name: string;
26
28
  icon: string;
27
29
  list: string;
30
+ context: boolean;
28
31
  read: string;
29
32
  /** The provider's read limit; a consumer combines it with its own. */
30
33
  maxBytes: number;
@@ -61,6 +64,14 @@ const issuesOf = (app: CapabilityCatalogApp): FileProviderIssue[] | null => {
61
64
  }
62
65
  };
63
66
 
67
+ const acceptsContext = (app: CapabilityCatalogApp): boolean => {
68
+ try {
69
+ return fileProviderAcceptsContext(app.manifest);
70
+ } catch {
71
+ return false;
72
+ }
73
+ };
74
+
64
75
  /**
65
76
  * Keeps the catalog apps that offer files with a usable `list` and `read`. Core already drops invalid declarations;
66
77
  * the check repeats here because a manifest from another Cloud release may name an operation this reader left out.
@@ -81,6 +92,7 @@ export const fileProviderSources = (apps: readonly CapabilityCatalogApp[], local
81
92
  name: app.appName,
82
93
  icon: providerIcon(app.appIcon, "ti ti-folders"),
83
94
  list: declaration.list,
95
+ context: acceptsContext(app),
84
96
  read: declaration.read,
85
97
  maxBytes: read.stream.maxBytes,
86
98
  ...(save?.stream ? { save: { id: save.localId, maxBytes: save.stream.maxBytes } } : {}),
@@ -112,7 +124,7 @@ export type FileProviderPage = { items: FileProviderEntry[]; next: string | null
112
124
  /** One folder page. Without `parent` it is the provider's root. */
113
125
  export const listProviderFolder = async (
114
126
  provider: FileProviderSource,
115
- input: { parent?: string; query?: string; cursor?: string },
127
+ input: { parent?: string; query?: string; cursor?: string; context?: FileProviderContext },
116
128
  caller: FileProviderCaller,
117
129
  signal?: AbortSignal,
118
130
  ): Promise<FileProviderPage> => {
@@ -125,6 +137,7 @@ export const listProviderFolder = async (
125
137
  ...(input.parent === undefined ? {} : { parent: input.parent }),
126
138
  ...(input.query ? { query: input.query } : {}),
127
139
  ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
140
+ ...(provider.context === true && input.context !== undefined ? { context: input.context } : {}),
128
141
  limit: FILE_PROVIDER_PAGE_SIZE,
129
142
  },
130
143
  signal,
@@ -0,0 +1,24 @@
1
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
2
+
3
+ export function isPanelReadyMessage(value: unknown, provider: string): value is { type: "cloud.panel.ready"; v: 1; provider: string } {
4
+ return (
5
+ isRecord(value) &&
6
+ Reflect.ownKeys(value).length === 3 &&
7
+ Object.hasOwn(value, "type") &&
8
+ Object.hasOwn(value, "v") &&
9
+ Object.hasOwn(value, "provider") &&
10
+ value.type === "cloud.panel.ready" &&
11
+ value.v === 1 &&
12
+ typeof value.provider === "string" &&
13
+ value.provider === provider
14
+ );
15
+ }
16
+
17
+ export function isPanelCloseMessage(value: unknown): value is { type: "cloud.panel.close"; v?: 1 } {
18
+ return (
19
+ isRecord(value) &&
20
+ Object.hasOwn(value, "type") &&
21
+ value.type === "cloud.panel.close" &&
22
+ (Reflect.ownKeys(value).length === 1 || (Reflect.ownKeys(value).length === 2 && Object.hasOwn(value, "v") && value.v === 1))
23
+ );
24
+ }