@k2b/cloud 0.6.0 → 0.8.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 (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -28,11 +28,13 @@ const pinSchema = z
28
28
  const passkeySchema = z
29
29
  .object({ type: z.literal("passkey"), credentialId: z.string().min(1).max(2048), salt: base64(32), wrapped: blobSchema })
30
30
  .strict();
31
- const methodSchema = z.discriminatedUnion("type", [pinSchema, passkeySchema]);
31
+ const localSchema = z.object({ type: z.literal("local"), key: base64(32) }).strict();
32
+ const methodSchema = z.discriminatedUnion("type", [pinSchema, passkeySchema, localSchema]);
32
33
  const configSchema = z
33
34
  .object({ version: z.literal(1), id: base64(16), methods: z.array(methodSchema).min(1).max(2) })
34
35
  .strict()
35
- .refine((c) => new Set(c.methods.map((m) => m.type)).size === c.methods.length);
36
+ .refine((c) => new Set(c.methods.map((m) => m.type)).size === c.methods.length)
37
+ .refine((c) => !c.methods.some((m) => m.type === "local") || c.methods.length === 1);
36
38
  export type AppVaultConfig = z.infer<typeof configSchema>;
37
39
  export type AppVaultBlob = z.infer<typeof blobSchema>;
38
40
  export type AppVaultMethod = AppVaultConfig["methods"][number];
@@ -89,7 +91,7 @@ async function pinKey(pin: string, salt: string) {
89
91
  function wrapContext(id: string, method: Omit<z.infer<typeof pinSchema>, "wrapped"> | Omit<z.infer<typeof passkeySchema>, "wrapped">) {
90
92
  return JSON.stringify(["cloud-login-vault", 1, id, method.type, method.salt, method.type === "pin" ? method.kdf : method.credentialId]);
91
93
  }
92
- function methodContext(id: string, method: AppVaultMethod) {
94
+ function methodContext(id: string, method: Exclude<AppVaultMethod, { type: "local" }>) {
93
95
  return wrapContext(
94
96
  id,
95
97
  method.type === "pin"
@@ -238,6 +240,11 @@ function session(id: string, raw: Uint8Array<ArrayBuffer>, key: CryptoKey) {
238
240
  plain.fill(0);
239
241
  }
240
242
  },
243
+ /** Persists the vault key without PIN protection. Never combine with protected methods. */
244
+ local(): AppVaultMethod {
245
+ check();
246
+ return { type: "local", key: encode(raw) };
247
+ },
241
248
  async pin(pin: string): Promise<AppVaultMethod> {
242
249
  check();
243
250
  const method = { type: "pin" as const, salt: encode(bytes(16)), kdf: "argon2id-64m-t3-p1" as const };
@@ -281,13 +288,18 @@ async function create() {
281
288
  const raw = bytes(32);
282
289
  return session(encode(bytes(16)), raw, await aes(raw));
283
290
  }
284
- async function unlock(config: AppVaultConfig, type: "pin" | "passkey", pin?: string, signal?: AbortSignal) {
291
+ async function unlock(config: AppVaultConfig, type: AppVaultMethod["type"], pin?: string, signal?: AbortSignal) {
285
292
  const value = configSchema.parse(config);
286
293
  const method = value.methods.find((m) => m.type === type);
287
294
  if (!method) throw invalid();
288
- const wrapping =
289
- method.type === "pin" ? await pinKey(pin ?? "", method.salt) : await passkeyKey(method.credentialId, method.salt, signal);
290
- const raw = await decrypt(wrapping, method.wrapped, methodContext(value.id, method));
295
+ const raw =
296
+ method.type === "local"
297
+ ? decode(method.key)
298
+ : await decrypt(
299
+ method.type === "pin" ? await pinKey(pin ?? "", method.salt) : await passkeyKey(method.credentialId, method.salt, signal),
300
+ method.wrapped,
301
+ methodContext(value.id, method),
302
+ );
291
303
  if (raw.length !== 32) {
292
304
  raw.fill(0);
293
305
  throw invalid();
@@ -0,0 +1,109 @@
1
+ import type { z } from "zod";
2
+ import type { CommandOptions } from "../contracts/commands";
3
+ import type { GlobalSearchOptions } from "./search-bridge";
4
+
5
+ export type CommandTarget = { command: string; input: unknown; options?: CommandOptions };
6
+ export type ContextAwareCommand = {
7
+ id: string;
8
+ title: string;
9
+ description: string;
10
+ icon?: string;
11
+ /** Selection actions appear before page actions. Defaults to page. */
12
+ scope?: "selection" | "page";
13
+ /** One platform-aware key combination, for example mod+shift+k. */
14
+ shortcut?: string;
15
+ /** A linkable app Command, an in-place search, or a local UI interaction. */
16
+ action: CommandTarget | { search: GlobalSearchOptions } | (() => void | Promise<void>);
17
+ };
18
+ type HandleRequest = { target: CommandTarget; handlers: Array<() => void | Promise<void>> };
19
+ type CollectRequest = { commands: ContextAwareCommand[] };
20
+ const HANDLE = "cloud.command.handle";
21
+ const COLLECT = "cloud.command.collect";
22
+ const EXECUTE = "cloud.command.execute";
23
+ type ExecuteRequest = { command: ContextAwareCommand; run: () => Promise<void>; result?: Promise<void> };
24
+ export const COMMANDS_CHANGED = "cloud.command.changed";
25
+
26
+ /** Document events work across independently hydrated islands and bundles. */
27
+ export const registerCommandHandler = <S extends z.ZodType>(
28
+ command: string,
29
+ schema: S,
30
+ handler: (input: z.output<S>, options: CommandOptions) => void | Promise<void>,
31
+ accepts: (input: z.output<S>) => boolean = () => true,
32
+ ): (() => void) => {
33
+ let active = true;
34
+ const listener = (event: Event) => {
35
+ const request = (event as CustomEvent<HandleRequest>).detail;
36
+ if (request.target.command !== command) return;
37
+ const parsed = schema.safeParse(request.target.input);
38
+ if (!parsed.success || !accepts(parsed.data)) return;
39
+ // Collect owners before invoking one, so overlapping islands cannot choose by listener order.
40
+ request.handlers.push(() => {
41
+ if (!active) throw new Error("Command context is no longer available");
42
+ return handler(parsed.data, request.target.options ?? {});
43
+ });
44
+ };
45
+ window.addEventListener(HANDLE, listener);
46
+ return () => {
47
+ active = false;
48
+ window.removeEventListener(HANDLE, listener);
49
+ };
50
+ };
51
+ export const requestCommandHandling = (target: CommandTarget): Promise<void> | undefined => {
52
+ const detail: HandleRequest = { target, handlers: [] };
53
+ window.dispatchEvent(new CustomEvent(HANDLE, { detail }));
54
+ if (detail.handlers.length > 1) return Promise.reject(new Error("Multiple Command owners accepted the request"));
55
+ const handler = detail.handlers[0];
56
+ return handler ? Promise.resolve().then(handler) : undefined;
57
+ };
58
+
59
+ /** Register within a reactive effect and dispose it when the visible context changes. */
60
+ export const registerContextAwareCommand = (command: ContextAwareCommand): (() => void) => {
61
+ let active = true;
62
+ let pending: Promise<void> | undefined;
63
+ const registered = { ...command };
64
+ const execute = (event: Event) => {
65
+ const request = (event as CustomEvent<ExecuteRequest>).detail;
66
+ if (request.command.id !== registered.id || request.command.action !== registered.action) return;
67
+ if (pending) {
68
+ request.result = Promise.resolve();
69
+ return;
70
+ }
71
+ pending = Promise.resolve()
72
+ .then(() => {
73
+ if (!active) throw new Error("Command context is no longer available");
74
+ return request.run();
75
+ })
76
+ .finally(() => {
77
+ pending = undefined;
78
+ });
79
+ request.result = pending;
80
+ };
81
+ const listener = (event: Event) => {
82
+ const request = (event as CustomEvent<CollectRequest>).detail;
83
+ request.commands.push(registered);
84
+ };
85
+ window.addEventListener(COLLECT, listener);
86
+ window.addEventListener(EXECUTE, execute);
87
+ window.dispatchEvent(new Event(COMMANDS_CHANGED));
88
+ return () => {
89
+ active = false;
90
+ window.removeEventListener(COLLECT, listener);
91
+ window.removeEventListener(EXECUTE, execute);
92
+ window.dispatchEvent(new Event(COMMANDS_CHANGED));
93
+ };
94
+ };
95
+ export const collectContextAwareCommands = (): ContextAwareCommand[] => {
96
+ if (typeof window === "undefined") return [];
97
+ const detail: CollectRequest = { commands: [] };
98
+ window.dispatchEvent(new CustomEvent(COLLECT, { detail }));
99
+ // Ambiguous IDs are not executable, regardless of island hydration order.
100
+ return detail.commands
101
+ .filter((command) => detail.commands.filter((other) => other.id === command.id).length === 1)
102
+ .sort((a, b) => Number(b.scope === "selection") - Number(a.scope === "selection"));
103
+ };
104
+
105
+ export const requestContextCommandExecution = (command: ContextAwareCommand, run: () => Promise<void>): Promise<void> | undefined => {
106
+ const detail: ExecuteRequest = { command, run };
107
+ window.dispatchEvent(new CustomEvent(EXECUTE, { detail }));
108
+ return detail.result;
109
+ };
@@ -0,0 +1,76 @@
1
+ import { COMMANDS_CHANGED, type ContextAwareCommand, collectContextAwareCommands } from "./command-bridge";
2
+
3
+ const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
4
+ const modifiers = ["ctrl", "alt", "shift", "meta"] as const;
5
+ type Combination = { key: string; ctrl: boolean; alt: boolean; shift: boolean; meta: boolean };
6
+
7
+ const parse = (shortcut: string): Combination | undefined => {
8
+ const parts = shortcut.toLowerCase().split("+");
9
+ const key = parts.pop();
10
+ if (!key || parts.some((part) => !["mod", ...modifiers].includes(part))) return;
11
+ const keys = new Set(parts.map((part) => (part === "mod" ? (isMac() ? "meta" : "ctrl") : part)));
12
+ return { key, ctrl: keys.has("ctrl"), alt: keys.has("alt"), shift: keys.has("shift"), meta: keys.has("meta") };
13
+ };
14
+ const signature = (shortcut: string) => {
15
+ const parsed = parse(shortcut);
16
+ return parsed && JSON.stringify(parsed);
17
+ };
18
+
19
+ /** Shared projection for the dispatcher, palette and Help. Conflicts never choose a winner. */
20
+ export const contextCommandsWithShortcuts = (): ContextAwareCommand[] => {
21
+ const commands = collectContextAwareCommands();
22
+ const keys = commands.map((command) => (command.shortcut ? signature(command.shortcut) : undefined));
23
+ return commands.map((command, index) => ({
24
+ ...command,
25
+ shortcut: keys[index] && keys.filter((key) => key === keys[index]).length === 1 ? command.shortcut : undefined,
26
+ }));
27
+ };
28
+
29
+ export const shortcutLabel = (shortcut: string): string => {
30
+ const combination = parse(shortcut);
31
+ if (!combination) return "";
32
+ const mac = isMac();
33
+ const names = { ctrl: mac ? "⌃" : "Ctrl", alt: mac ? "⌥" : "Alt", shift: mac ? "⇧" : "Shift", meta: mac ? "⌘" : "Meta" };
34
+ return [...modifiers.filter((key) => combination[key]).map((key) => names[key]), combination.key.toUpperCase()].join(mac ? "" : "+");
35
+ };
36
+
37
+ /** One owner in the Cloud layout; app islands only publish commands. */
38
+ export const attachCommandShortcuts = (run: (command: ContextAwareCommand) => void): (() => void) => {
39
+ const listener = (event: KeyboardEvent) => {
40
+ if (event.defaultPrevented || event.repeat || event.isComposing || (!isMac() && event.getModifierState("AltGraph"))) return;
41
+ const target = event.target;
42
+ const editing =
43
+ target instanceof Element &&
44
+ Boolean(target.closest("input, textarea, select, [contenteditable]:not([contenteditable=false]), [role=textbox]"));
45
+ const dialogOpen = Boolean(document.querySelector('dialog[open], [role="dialog"][aria-modal="true"]'));
46
+ const matches = contextCommandsWithShortcuts().filter((command) => {
47
+ if (!command.shortcut) return false;
48
+ if (command.id !== "cloud.search" && dialogOpen) return false;
49
+ const combo = parse(command.shortcut);
50
+ if (!combo || (editing && !combo.ctrl && !combo.meta)) return false;
51
+ // Shift+/ reports '?' on common keyboard layouts.
52
+ const key = event.key === "?" && combo.key === "/" ? "/" : event.key.toLowerCase();
53
+ const physicalLetter =
54
+ combo.alt &&
55
+ (event.key === "Dead" || !/^[a-z]$/i.test(event.key)) &&
56
+ /^[a-z]$/.test(combo.key) &&
57
+ event.code === `Key${combo.key.toUpperCase()}`;
58
+ return (
59
+ (key === combo.key || physicalLetter) &&
60
+ event.ctrlKey === combo.ctrl &&
61
+ event.altKey === combo.alt &&
62
+ event.shiftKey === combo.shift &&
63
+ event.metaKey === combo.meta
64
+ );
65
+ });
66
+ if (matches.length !== 1) return;
67
+ event.preventDefault();
68
+ event.stopPropagation();
69
+ run(matches[0]!);
70
+ };
71
+ // Registered app combinations must win over editor bindings such as Mod+Shift+K.
72
+ window.addEventListener("keydown", listener, true);
73
+ return () => window.removeEventListener("keydown", listener, true);
74
+ };
75
+
76
+ export { COMMANDS_CHANGED };
@@ -0,0 +1,67 @@
1
+ import { toast } from "@k2b/ui";
2
+ import { resolveCommand } from "../capabilities/client";
3
+ import { type CommandOptions, CommandOptionsSchema, clearCommand, readCommand } from "../contracts/commands";
4
+ import {
5
+ type ContextAwareCommand,
6
+ collectContextAwareCommands,
7
+ requestCommandHandling,
8
+ requestContextCommandExecution,
9
+ } from "./command-bridge";
10
+ import { resourceSearchMessages } from "./resource-search-messages";
11
+ import { requestGlobalSearch } from "./search-bridge";
12
+
13
+ export type { CommandOptions } from "../contracts/commands";
14
+ export type { CommandTarget, ContextAwareCommand } from "./command-bridge";
15
+ export { registerCommandHandler, registerContextAwareCommand } from "./command-bridge";
16
+
17
+ /** Opens locally only when a mounted owner explicitly accepts; otherwise follows the app's URL. */
18
+ export const openCommand = async (command: string, input: unknown = {}, options: CommandOptions = {}): Promise<void> => {
19
+ const parsedOptions = CommandOptionsSchema.parse(options);
20
+ const handled = requestCommandHandling({ command, input, options: parsedOptions });
21
+ if (handled) return handled;
22
+ const result = await resolveCommand(command, input, parsedOptions);
23
+ if (!result.ok) throw new Error(result.error.message);
24
+ window.location.assign(result.data.href);
25
+ };
26
+
27
+ /** Call after this page's handlers have mounted. Each entry is consumed, including invalid links. */
28
+ export const consumeCommandLink = async (): Promise<void> => {
29
+ const url = new URL(window.location.href);
30
+ const messages = resourceSearchMessages.resolve([document.documentElement.lang]).t;
31
+ let target: ReturnType<typeof readCommand>;
32
+ try {
33
+ target = readCommand(url);
34
+ } catch {
35
+ window.history.replaceState(window.history.state, "", clearCommand(url));
36
+ toast.error(messages.commandFailed);
37
+ return;
38
+ }
39
+ if (!target) return;
40
+ window.history.replaceState(window.history.state, "", clearCommand(url));
41
+ try {
42
+ const handled = requestCommandHandling({ command: target.id, input: target.input, options: target.options });
43
+ if (!handled) {
44
+ toast.error(messages.commandUnavailable);
45
+ return;
46
+ }
47
+ await handled;
48
+ } catch (error) {
49
+ toast.error(error instanceof Error && error.message ? error.message : messages.commandFailed);
50
+ }
51
+ };
52
+
53
+ /** The palette and keyboard share live ownership checks, pending guards and error feedback. */
54
+ export const runContextAwareCommand = async (command: ContextAwareCommand): Promise<void> => {
55
+ const messages = resourceSearchMessages.resolve([document.documentElement.lang]).t;
56
+ try {
57
+ const current = collectContextAwareCommands().find((item) => item.id === command.id && item.action === command.action);
58
+ if (!current) throw new Error(messages.commandUnavailable);
59
+ await requestContextCommandExecution(current, async () => {
60
+ if (typeof current.action === "function") await current.action();
61
+ else if ("search" in current.action) await requestGlobalSearch({ query: "", ...current.action.search });
62
+ else await openCommand(current.action.command, current.action.input, current.action.options);
63
+ });
64
+ } catch (error) {
65
+ toast.error(error instanceof Error && error.message ? error.message : messages.commandFailed);
66
+ }
67
+ };
@@ -0,0 +1,48 @@
1
+ import { fuzzy } from "@k2b/stdlib";
2
+ import type { SearchItem } from "../api/search/schemas";
3
+ import type { AppMeta } from "../contracts/app";
4
+
5
+ /** UI-only rows. The synthetic ref is a row key, never a readable resource. */
6
+ export type NavigationSearchItem = SearchItem & { keywords?: readonly string[] };
7
+
8
+ export const navigationSearchItems = (apps: readonly AppMeta[], visibleAppIds: readonly string[]): NavigationSearchItem[] => {
9
+ const visible = new Set(visibleAppIds);
10
+ return apps
11
+ .filter((app) => visible.has(app.id))
12
+ .flatMap((app) =>
13
+ (app.searchLinks ?? []).map((link) => ({
14
+ appId: app.id,
15
+ appName: app.name,
16
+ appIcon: app.icon,
17
+ ref: { type: "cloud.navigation", id: `${app.id}:${link.href}` },
18
+ readable: false,
19
+ title: link.label,
20
+ preview: link.description,
21
+ href: link.href,
22
+ icon: link.icon ?? app.icon,
23
+ priority: 0,
24
+ keywords: link.keywords,
25
+ })),
26
+ );
27
+ };
28
+
29
+ export const matchNavigationSearchItems = (
30
+ items: readonly NavigationSearchItem[],
31
+ input: { query: string; tags: readonly string[]; appId?: string | null; requireReader?: boolean },
32
+ ): NavigationSearchItem[] => {
33
+ const query = input.query.trim().toLowerCase();
34
+ if (input.requireReader || input.tags.length > 0 || (!input.appId && query.length < 2)) return [];
35
+ if (input.appId) {
36
+ const scoped = items.filter((item) => item.appId === input.appId);
37
+ return query
38
+ ? fuzzy
39
+ .filter(query, scoped, { key: (item) => [item.title, item.preview ?? "", ...(item.keywords ?? [])].join(" ") })
40
+ .map((hit) => hit.item)
41
+ : scoped;
42
+ }
43
+ const words = query.split(/\s+/);
44
+ return items.filter((item) => {
45
+ const text = [item.title, item.preview ?? "", ...(item.keywords ?? [])].join(" ").toLowerCase();
46
+ return words.every((word) => text.includes(word));
47
+ });
48
+ };
@@ -56,7 +56,7 @@ const subscriptionPayload = (subscription: PushSubscription): BrowserPushSubscri
56
56
  };
57
57
  };
58
58
 
59
- const responseError = async (response: Response, fallback: string): Promise<Error> => {
59
+ const responseError = async (response: Pick<Response, "json">, fallback: string): Promise<Error> => {
60
60
  try {
61
61
  const payload = (await response.json()) as { message?: unknown };
62
62
  if (typeof payload.message === "string" && payload.message.trim()) return new Error(payload.message);
@@ -1,11 +1,11 @@
1
- import { prompts } from "@k2b/ui";
1
+ import { dialogCore } from "@k2b/ui";
2
2
  import type { SearchItem } from "../api/search/schemas";
3
3
  import type { CloudResourceRef } from "../contracts";
4
4
  import CloudResourceSearch from "./CloudResourceSearch";
5
+ import { resourceSearchDialogOptions } from "./resource-search-dialog";
5
6
  import { resourceSearchMessages } from "./resource-search-messages";
6
7
 
7
8
  export type CloudResourcePickerItem = SearchItem;
8
-
9
9
  export type CloudResourcePickerOptions = {
10
10
  title?: string;
11
11
  placeholder?: string;
@@ -17,23 +17,10 @@ export type CloudResourcePickerOptions = {
17
17
  export const openCloudResourcePicker = (options: CloudResourcePickerOptions = {}): Promise<CloudResourcePickerItem | undefined> => {
18
18
  const locale = typeof document === "undefined" ? "en" : document.documentElement.lang || "en";
19
19
  const t = resourceSearchMessages.resolve([locale]).t;
20
- return prompts.dialog<CloudResourcePickerItem>(
20
+ return dialogCore.open<CloudResourcePickerItem>(
21
21
  (close) => (
22
- <div class="k2b-dialog__body h-[min(36rem,var(--ui-dialog-available-height))] min-h-0 p-0">
23
- <CloudResourceSearch
24
- initialAppId={options.initialAppId}
25
- excludeRefs={options.excludeRefs}
26
- requireReader={options.requireReader}
27
- placeholder={options.placeholder ?? t.searchPlaceholder}
28
- onSelect={close}
29
- />
30
- </div>
22
+ <CloudResourceSearch {...options} title={options.title ?? t.chooseResource} selectionMode onSelect={close} onClose={() => close()} />
31
23
  ),
32
- {
33
- title: options.title ?? t.chooseResource,
34
- icon: "ti ti-cloud-search",
35
- size: "large",
36
- ariaLabel: options.title ?? t.chooseResource,
37
- },
24
+ resourceSearchDialogOptions(options.title ?? t.chooseResource),
38
25
  );
39
26
  };
@@ -0,0 +1,9 @@
1
+ import type { OpenDialogOptions } from "@k2b/ui";
2
+
3
+ /** One shell for global navigation and resource selection, with native focus/escape handling. */
4
+ export const resourceSearchDialogOptions = (ariaLabel: string): OpenDialogOptions => ({
5
+ panelClassName: "cloud-search-dialog",
6
+ contentClassName: "cloud-search-dialog__content",
7
+ initialFocus: "first-input",
8
+ ariaLabel,
9
+ });
@@ -0,0 +1,46 @@
1
+ import type { SearchApp } from "../api/search/schemas";
2
+
3
+ export type SearchTag = NonNullable<SearchApp["tags"]>[number] & { appName: string; appIcon: string };
4
+
5
+ /** Keep aliases discoverable without listing the same facet repeatedly. */
6
+ export const searchTags = (apps: readonly SearchApp[], appId?: string | null): SearchTag[] => {
7
+ const seen = new Set<string>();
8
+ return apps
9
+ .filter((app) => !appId || app.id === appId)
10
+ .flatMap((app) =>
11
+ (app.tags ?? []).flatMap((tag) => {
12
+ if (seen.has(tag.tag)) return [];
13
+ seen.add(tag.tag);
14
+ return [{ ...tag, appName: app.name, appIcon: app.icon }];
15
+ }),
16
+ );
17
+ };
18
+
19
+ export const tagAtCursor = (text: string, caret: number) => {
20
+ const pos = Math.min(caret, text.length);
21
+ let start = pos;
22
+ let end = pos;
23
+ while (start > 0 && !/\s/.test(text[start - 1]!)) start--;
24
+ while (end < text.length && !/\s/.test(text[end]!)) end++;
25
+ if (text[start] !== "#" || pos <= start || text.slice(start + 1, end).includes("#")) return null;
26
+ return { start, end, prefix: text.slice(start + 1, pos).toLowerCase() };
27
+ };
28
+
29
+ /** Only whitespace commits a typed/pasted tag; an unfinished #prefix is not an error. */
30
+ export const commitTypedTags = (text: string, caret: number) => {
31
+ const tags: string[] = [];
32
+ let nextCaret = caret;
33
+ const input = text.replace(/(^|\s)#([^\s#]+)(?=\s)/g, (match: string, space: string, tag: string, offset: number) => {
34
+ tags.push(tag.toLowerCase());
35
+ if (offset < caret) nextCaret -= Math.min(match.length - space.length, caret - offset - space.length);
36
+ return space;
37
+ });
38
+ return { input, caret: Math.max(0, nextCaret), tags };
39
+ };
40
+
41
+ export const matchingSearchTags = (tags: readonly SearchTag[], prefix: string, selected: readonly string[]) =>
42
+ tags.filter(
43
+ (tag) =>
44
+ !selected.some((value) => value === tag.tag || tag.aliases?.includes(value)) &&
45
+ (!prefix || [tag.tag, tag.title.toLowerCase(), ...(tag.aliases ?? [])].some((value) => value.startsWith(prefix))),
46
+ );
@@ -4,52 +4,88 @@ export const resourceSearchMessages = i18n.define({
4
4
  baseLocale: "en",
5
5
  messages: {
6
6
  en: {
7
- searchFailed: "Search failed.",
8
- searchPlaceholder: "Search across apps…",
7
+ actions: "Actions",
8
+ contextActions: "On this page",
9
+ commandPlaceholder: "Find an action…",
10
+ commandFailed: "Could not open this action. Try again.",
11
+ commandUnavailable: "This action is no longer available. Choose another action.",
12
+ commandHint: "Choose an action, or type > next time.",
13
+ searchFailed: "Search is currently unavailable. Try again.",
14
+ navigationFailed: "Could not open this result. Try again.",
15
+ partialFailure: "Some search sources are unavailable. Results may be incomplete.",
16
+ searchPlaceholder: "Search Cloud…",
17
+ scopedSearchPlaceholder: "Search…",
9
18
  searchCloudResources: "Search Cloud resources",
10
- tagSuggestions: "Tag suggestions",
11
- filterByApp: "Filter by app",
12
- allApps: "All apps",
13
- removeTag: ({ tag }: { tag: string }) => `Remove tag ${tag}`,
14
- removeTagTitle: ({ tag }: { tag: string }) => `Remove #${tag}`,
19
+ chooseResource: "Choose a resource",
20
+ pickerPlaceholder: "Find a resource…",
21
+ close: "Close search",
22
+ add: "Add",
23
+ chooseFirst: "Select a resource",
24
+ details: "Details",
25
+ back: "Back to results",
26
+ tagSuggestions: "Search filters",
27
+ allTags: "All filters",
28
+ backToSearch: "Back to search",
29
+ navigationHint: "Search available pages.",
30
+ startHint: "Find content, or narrow your search with a tag.",
31
+ pickerHint: "Find a note, file, or another resource.",
32
+ noMatches: "No matches. Try another search term.",
33
+ noTags: "No matching filter.",
34
+ unsupportedTags: "Unknown filter. Remove it or choose another.",
35
+ removeTag: ({ tag }: { tag: string }) => `Remove #${tag}`,
36
+ removeScope: ({ app }: { app: string }) => `Search beyond ${app}`,
15
37
  resultCount: ({ count }: { count: number }) => i18n.plural(count, "en", { one: `${count} result`, other: `${count} results` }),
16
- tagHelp: "tag help",
17
- typeToSearchBeforeTag: "Type to search, or use a",
18
- typeToSearchAfterTag: "to focus on one app.",
19
- noTagsAvailable: "No tags available.",
20
- addTag: ({ tag, app }: { tag: string; app: string }) => `Add #${tag} (${app})`,
21
- seeAllTagDescriptions: "See all tag descriptions",
22
- noAppSupports: "No app supports",
23
- removeTagOrPick: "Remove the tag or pick one below.",
24
- removeNamedTag: ({ tag }: { tag: string }) => `Remove #${tag}`,
25
- noMatches: "No matches",
26
- matchesFor: "for",
27
- selectPreview: "Select a result to preview details.",
28
- chooseResource: "Choose Cloud resource",
38
+ retry: "Try again",
39
+ navigate: "navigate",
40
+ open: "open",
41
+ newTab: "new tab",
42
+ homeHint: "Back to center",
43
+ releaseHint: "Release to center",
44
+ select: "select",
45
+ loading: "Searching…",
46
+ loadingTags: "Loading filters…",
29
47
  },
30
48
  de: {
31
- searchFailed: "Die Suche ist fehlgeschlagen.",
32
- searchPlaceholder: "Apps durchsuchen…",
49
+ actions: "Aktionen",
50
+ contextActions: "Auf dieser Seite",
51
+ commandPlaceholder: "Aktion finden…",
52
+ commandFailed: "Die Aktion konnte nicht geöffnet werden. Versuche es erneut.",
53
+ commandUnavailable: "Diese Aktion ist nicht mehr verfügbar. Wähle eine andere Aktion.",
54
+ commandHint: "Wähle eine Aktion – beim nächsten Mal geht auch >.",
55
+ searchFailed: "Suche gerade nicht erreichbar. Versuche es erneut.",
56
+ navigationFailed: "Der Treffer konnte nicht geöffnet werden. Versuche es erneut.",
57
+ partialFailure: "Einige Suchquellen sind nicht erreichbar. Ergebnisse können unvollständig sein.",
58
+ searchPlaceholder: "Cloud durchsuchen…",
59
+ scopedSearchPlaceholder: "Durchsuchen…",
33
60
  searchCloudResources: "Cloud-Inhalte durchsuchen",
34
- tagSuggestions: "Tag-Vorschläge",
35
- filterByApp: "Nach App filtern",
36
- allApps: "Alle Apps",
37
- removeTag: ({ tag }) => `Tag ${tag} entfernen`,
38
- removeTagTitle: ({ tag }) => `#${tag} entfernen`,
61
+ chooseResource: "Ressource wählen",
62
+ pickerPlaceholder: "Ressource suchen…",
63
+ close: "Suche schließen",
64
+ add: "Hinzufügen",
65
+ chooseFirst: "Eine Ressource auswählen",
66
+ details: "Details",
67
+ back: "Zurück zu Ergebnissen",
68
+ tagSuggestions: "Suchfilter",
69
+ allTags: "Alle Filter",
70
+ backToSearch: "Zurück zur Suche",
71
+ navigationHint: "Verfügbare Seiten durchsuchen.",
72
+ startHint: "Inhalte finden oder mit einem Tag eingrenzen.",
73
+ pickerHint: "Finde eine Notiz, Datei oder einen anderen Inhalt.",
74
+ noMatches: "Keine Treffer. Versuche einen anderen Suchbegriff.",
75
+ noTags: "Kein passender Filter.",
76
+ unsupportedTags: "Unbekannter Filter. Entferne ihn oder wähle einen anderen.",
77
+ removeTag: ({ tag }) => `#${tag} entfernen`,
78
+ removeScope: ({ app }) => `Außerhalb von ${app} suchen`,
39
79
  resultCount: ({ count }) => i18n.plural(count, "de", { one: `${count} Ergebnis`, other: `${count} Ergebnisse` }),
40
- tagHelp: "Hilfe zu Tags",
41
- typeToSearchBeforeTag: "Gib einen Suchbegriff ein oder verwende einen",
42
- typeToSearchAfterTag: "für die Suche in einer bestimmten App.",
43
- noTagsAvailable: "Keine Tags verfügbar.",
44
- addTag: ({ tag, app }) => `#${tag} hinzufügen (${app})`,
45
- seeAllTagDescriptions: "Alle Tag-Beschreibungen anzeigen",
46
- noAppSupports: "Keine App unterstützt",
47
- removeTagOrPick: "Entferne den Tag oder wähle unten einen anderen aus.",
48
- removeNamedTag: ({ tag }) => `#${tag} entfernen`,
49
- noMatches: "Keine Treffer",
50
- matchesFor: "für",
51
- selectPreview: "Wähle ein Ergebnis aus, um Details anzuzeigen.",
52
- chooseResource: "Cloud-Inhalt auswählen",
80
+ retry: "Erneut versuchen",
81
+ navigate: "navigieren",
82
+ open: "öffnen",
83
+ newTab: "neuer Tab",
84
+ homeHint: "Zurück zur Mitte",
85
+ releaseHint: "Loslassen zum Zentrieren",
86
+ select: "auswählen",
87
+ loading: "Suche läuft…",
88
+ loadingTags: "Filter werden geladen…",
53
89
  },
54
90
  },
55
91
  });
@@ -8,12 +8,19 @@ export const cloudResourceSearchUrl = (request: {
8
8
  query: string;
9
9
  tags: readonly string[];
10
10
  appId?: string | null;
11
+ scope?: CloudResourceRef;
12
+ scopeTag?: string;
11
13
  requireReader?: boolean;
12
14
  }): string => {
13
15
  const params = new URLSearchParams({ provider_limit: String(PROVIDER_LIMIT) });
14
16
  if (request.query.length > 0) params.set("q", request.query);
15
17
  for (const tag of request.tags) params.append("tag", tag);
16
18
  if (request.appId) params.set("app", request.appId);
19
+ if (request.scopeTag !== undefined) params.set("scope_tag", request.scopeTag);
20
+ if (request.scope) {
21
+ params.set("scope_type", request.scope.type);
22
+ params.set("scope_id", request.scope.id);
23
+ }
17
24
  if (request.requireReader) params.set("require_reader", "true");
18
25
  return `/api/search?${params.toString()}`;
19
26
  };