@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
package/src/api/search.ts CHANGED
@@ -2,7 +2,7 @@ import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { describeRoute, resolver } from "hono-openapi";
3
3
  import { readBoundedJson } from "../_internal/bounded-json";
4
4
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
5
- import { listCapabilities } from "../_internal/registry";
5
+ import { type listApps, listCapabilities } from "../_internal/registry";
6
6
  import type { CapabilityRegistryEntry } from "../contracts";
7
7
  import {
8
8
  CAPABILITY_FRAMEWORK_ERROR_CODES,
@@ -14,9 +14,12 @@ import {
14
14
  import { type AuthContext, auth, expectUserBackedActor, jsonResponse, preferredLocale, requiresAuth, v } from "../server";
15
15
  import { noTransform } from "../server/middleware/no-transform";
16
16
  import { logger } from "../services";
17
+ import { appAccessAuthority } from "../services/app-access";
18
+ import { discoverableAppEntries } from "../services/app-discovery";
17
19
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
18
20
  import { searchInvocationOperation } from "../services/identity/invocation-operations";
19
- import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
21
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
22
+ import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
20
23
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
21
24
  import { LOCALE_HEADER } from "../shared/locale";
22
25
  import { ndjsonLine, ndjsonStream, ndjsonStreamHeaders, startBounded, waitWithin } from "./fanout";
@@ -83,6 +86,7 @@ type SearchRouteDependencies = {
83
86
  listCapabilities?: () => Promise<CapabilityRegistryEntry[]>;
84
87
  fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
85
88
  authenticate?: MiddlewareHandler<AuthContext>;
89
+ listApps?: typeof listApps;
86
90
  signInvocation?: typeof signInvocationToken;
87
91
  withActiveSigner?: typeof withActiveIdentitySigner;
88
92
  };
@@ -137,7 +141,7 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
137
141
  const invocationAuthority = invocationAuthorityFromRequest(auth.getAuthority(c));
138
142
  let entries: CapabilityRegistryEntry[];
139
143
  try {
140
- entries = await registry();
144
+ entries = await discoverableAppEntries(appAccessAuthority(c), await registry(), dependencies.listApps);
141
145
  } catch (error) {
142
146
  log.warn("Search capability registry unavailable", { error: error instanceof Error ? error.message : String(error) });
143
147
  return c.json(
@@ -237,22 +241,25 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
237
241
  signedInvocations = await waitWithin(
238
242
  (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
239
243
  "invocation",
240
- (signer) =>
244
+ (signer, db) =>
241
245
  Promise.all(
242
246
  startBounded(
243
247
  active,
244
248
  PROVIDER_CONCURRENCY,
245
249
  (provider) =>
246
- (dependencies.signInvocation ?? signInvocationToken)({
247
- targetAppId: provider.appId,
248
- callingAppId: "core",
249
- operation: searchInvocationOperation,
250
- schemaHash: provider.schemaHash,
251
- authority: invocationAuthority,
252
- requestId,
253
- signer,
254
- issuer: signer.issuer,
255
- }),
250
+ (dependencies.signInvocation ?? signInvocationToken)(
251
+ {
252
+ targetAppId: provider.appId,
253
+ callingAppId: "core",
254
+ operation: searchInvocationOperation,
255
+ schemaHash: provider.schemaHash,
256
+ authority: invocationAuthority,
257
+ requestId,
258
+ signer,
259
+ issuer: signer.issuer,
260
+ },
261
+ { db, groupsAdmin: signer.groupsAdmin },
262
+ ),
256
263
  fanoutSignal,
257
264
  ),
258
265
  ),
@@ -2,10 +2,12 @@ import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { readBoundedJson } from "../_internal/bounded-json";
3
3
  import { getApp, listApps } from "../_internal/registry";
4
4
  import { type AuthContext, auth, rejectReservedWorkloadCredential } from "../server";
5
+ import { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/app-access";
5
6
  import { noTransform } from "../server/middleware/no-transform";
6
7
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
7
8
  import { syncInvocationOperation } from "../services/identity/invocation-operations";
8
- import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
9
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
10
+ import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
9
11
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
10
12
 
11
13
  type Dependencies = {
@@ -53,17 +55,20 @@ export const createSyncOpsProxyRoutes = (dependencies: Dependencies = {}) =>
53
55
  try {
54
56
  const signed = await (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
55
57
  "invocation",
56
- (signer) =>
57
- (dependencies.signInvocation ?? signInvocationToken)({
58
- targetAppId: app.id,
59
- callingAppId: "core",
60
- operation,
61
- schemaHash: null,
62
- authority: invocationAuthorityFromRequest(auth.getAuthority(c)),
63
- requestId,
64
- signer,
65
- issuer: signer.issuer,
66
- }),
58
+ (signer, db) =>
59
+ (dependencies.signInvocation ?? signInvocationToken)(
60
+ {
61
+ targetAppId: app.id,
62
+ callingAppId: "core",
63
+ operation,
64
+ schemaHash: null,
65
+ authority: invocationAuthorityFromRequest(auth.getAuthority(c)),
66
+ requestId,
67
+ signer,
68
+ issuer: signer.issuer,
69
+ },
70
+ { db, groupsAdmin: signer.groupsAdmin },
71
+ ),
67
72
  { signal, timeoutMs: 5_000 },
68
73
  );
69
74
  const headers = new Headers({ authorization: `Bearer ${signed.token}`, accept: "application/json" });
@@ -75,7 +80,8 @@ export const createSyncOpsProxyRoutes = (dependencies: Dependencies = {}) =>
75
80
  const result = await readBoundedJson(response, 4 * 1024 * 1024);
76
81
  if (!result.ok) return c.json({ message: "Invalid Sync operations response" }, 502);
77
82
  return Response.json(result.data, { status: response.status, headers: { "cache-control": "no-store" } });
78
- } catch {
83
+ } catch (error) {
84
+ if (error instanceof AppAccessDeniedError) return c.json(appAccessDeniedBody(error.app), 403);
79
85
  return c.json({ message: "Sync operations unavailable" }, 502);
80
86
  }
81
87
  });
@@ -1,8 +1,9 @@
1
+ import type { SQL } from "bun";
1
2
  import { type Context, Hono, type MiddlewareHandler } from "hono";
2
3
  import { bodyLimit } from "hono/body-limit";
3
4
  import { z } from "zod";
4
5
  import { readBoundedJson } from "../_internal/bounded-json";
5
- import { type DashboardWidget, listWidgets } from "../_internal/registry";
6
+ import { type DashboardWidget, type listApps, listWidgets } from "../_internal/registry";
6
7
  import { parseWidgetSettings } from "../_internal/widget-settings";
7
8
  import {
8
9
  type DashboardWidgetSize,
@@ -20,12 +21,16 @@ import {
20
21
  widgetKey,
21
22
  } from "../contracts/widgets";
22
23
  import { type AuthContext, auth, preferredLocale, rejectReservedWorkloadCredential } from "../server";
24
+ import { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/app-access";
23
25
  import { noTransform } from "../server/middleware/no-transform";
24
26
  import { WIDGET_SETTINGS_PARAM, WIDGET_SETTINGS_PARAM_MAX_LENGTH, WIDGET_SIZE_PARAM } from "../server/widget-request";
27
+ import { appAccessAuthority } from "../services/app-access";
28
+ import { discoverableAppEntries } from "../services/app-discovery";
25
29
  import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS } from "../services/identity/constants";
26
30
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
27
31
  import { widgetInvocationOperation } from "../services/identity/invocation-operations";
28
- import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
32
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
33
+ import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
29
34
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
30
35
  import { logger } from "../services/logging";
31
36
  import { LOCALE_HEADER } from "../shared/locale";
@@ -35,6 +40,7 @@ type WidgetRouteDependencies = {
35
40
  listWidgets?: typeof listWidgets;
36
41
  fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
37
42
  authenticate?: MiddlewareHandler<AuthContext>;
43
+ listApps?: typeof listApps;
38
44
  signInvocation?: typeof signInvocationToken;
39
45
  withActiveSigner?: typeof withActiveIdentitySigner;
40
46
  timeoutMs?: number;
@@ -196,17 +202,21 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
196
202
  widget: DashboardWidget,
197
203
  requestId: string | undefined,
198
204
  signer: Parameters<Parameters<typeof withActiveIdentitySigner>[1]>[0],
205
+ db: SQL,
199
206
  ) =>
200
- signInvocation({
201
- targetAppId: widget.appId,
202
- callingAppId: "core",
203
- operation: widgetInvocationOperation(widget.widgetId),
204
- schemaHash: null,
205
- authority: invocationAuthorityFromRequest(auth.getAuthority(c)),
206
- requestId,
207
- signer,
208
- issuer: signer.issuer,
209
- });
207
+ signInvocation(
208
+ {
209
+ targetAppId: widget.appId,
210
+ callingAppId: "core",
211
+ operation: widgetInvocationOperation(widget.widgetId),
212
+ schemaHash: null,
213
+ authority: invocationAuthorityFromRequest(auth.getAuthority(c)),
214
+ requestId,
215
+ signer,
216
+ issuer: signer.issuer,
217
+ },
218
+ { db, groupsAdmin: signer.groupsAdmin },
219
+ );
210
220
 
211
221
  const stream = async (c: Context<AuthContext>, entries: WidgetEntry[], requestId: string | undefined) => {
212
222
  const streamDeadline = AbortSignal.timeout(WIDGET_STREAM_DEADLINE_MS);
@@ -221,8 +231,10 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
221
231
  tokens = await waitWithin(
222
232
  withActiveSigner(
223
233
  "invocation",
224
- (signer) =>
225
- Promise.all(startBounded(entries, WIDGET_CONCURRENCY, async (entry) => sign(c, entry.widget, requestId, signer), signing)),
234
+ (signer, db) =>
235
+ Promise.all(
236
+ startBounded(entries, WIDGET_CONCURRENCY, async (entry) => sign(c, entry.widget, requestId, signer, db), signing),
237
+ ),
226
238
  { signal: signing, timeoutMs },
227
239
  ),
228
240
  signing,
@@ -316,7 +328,10 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
316
328
  let requested = new Map<string, DashboardWidgetSize | undefined>();
317
329
  try {
318
330
  const keys = new Set<string>();
319
- const declared = await waitWithin(registry(), setup);
331
+ const declared = await waitWithin(
332
+ registry().then((entries) => discoverableAppEntries(appAccessAuthority(c), entries, dependencies.listApps)),
333
+ setup,
334
+ );
320
335
  const declaredKeys = new Set(declared.map((widget) => widgetKey(widget.appId, widget.widgetId)));
321
336
  requested = new Map((c.req.queries("widget") ?? []).map((value) => parseRequestedWidget(value, declaredKeys)));
322
337
  widgets = declared.filter((widget) => {
@@ -351,7 +366,10 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
351
366
  const setup = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(timeoutMs)]);
352
367
  let declared: DashboardWidget[];
353
368
  try {
354
- declared = await waitWithin(registry(), setup);
369
+ declared = await waitWithin(
370
+ registry().then((entries) => discoverableAppEntries(appAccessAuthority(c), entries, dependencies.listApps)),
371
+ setup,
372
+ );
355
373
  } catch {
356
374
  log.warn("Widget registry unavailable", { reason: setup.aborted ? "deadline_exceeded" : "operation_failed" });
357
375
  return c.json({ message: "Widget registry is currently unavailable" }, 503);
@@ -372,12 +390,17 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
372
390
  const requestId = normalizeInvocationRequestId(c.req.header("x-request-id"));
373
391
  let phase: WidgetPhase = "registry";
374
392
  try {
375
- const widget = (await waitWithin(registry(), signal)).find((entry) => entry.appId === appId && entry.widgetId === widgetId);
393
+ const widget = (
394
+ await waitWithin(
395
+ registry().then((entries) => discoverableAppEntries(appAccessAuthority(c), entries, dependencies.listApps)),
396
+ signal,
397
+ )
398
+ ).find((entry) => entry.appId === appId && entry.widgetId === widgetId);
376
399
  if (!widget) return c.json({ message: "Widget not found" }, 404);
377
400
 
378
401
  phase = "signing";
379
402
  const signed = await waitWithin(
380
- withActiveSigner("invocation", (signer) => sign(c, widget, requestId, signer), { signal, timeoutMs }),
403
+ withActiveSigner("invocation", (signer, db) => sign(c, widget, requestId, signer, db), { signal, timeoutMs }),
381
404
  signal,
382
405
  );
383
406
 
@@ -405,6 +428,7 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
405
428
  : "Widget is unavailable";
406
429
  return Response.json({ message }, { status: 502 });
407
430
  } catch (error) {
431
+ if (error instanceof AppAccessDeniedError) return Response.json(appAccessDeniedBody(error.app), { status: 403 });
408
432
  const isTimeout = timeout.aborted || (error instanceof Error && error.name === "TimeoutError");
409
433
  const reason: WidgetRejectionReason = isTimeout ? "deadline_exceeded" : signal.aborted ? "request_cancelled" : "operation_failed";
410
434
  logRejection({ appId, widgetId }, phase, reason);
@@ -1,12 +1,9 @@
1
1
  import { dates } from "@k2b/stdlib";
2
2
  import { Button, dialogCore, PanelDialog, Placeholder, panelDialogOptions, prompts, useLocale } from "@k2b/ui";
3
3
  import { createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
4
- import { capabilityQuestionText, capabilityUnknownText } from "../_internal/capability-sentences";
5
- import { isLegacyActionReview, storedActionReview } from "../ai/action-review";
6
- import { type ActionCard, legacyActionCard, reviewActionCard } from "../ai/chat/action-card";
4
+ import type { ActionCard } from "../ai/chat/action-card";
7
5
  import { actionMessages } from "../ai/chat/action-messages";
8
6
  import { ActionQuestion, ActionReceipt } from "../ai/chat/action-question";
9
- import { aiChatMessages } from "../ai/chat/messages";
10
7
  import { refreshAppBadges } from "../browser/app-badges";
11
8
  import { coreClient } from "../clients/core";
12
9
  import {
@@ -17,6 +14,7 @@ import {
17
14
  ApprovalPageSchema,
18
15
  type PendingApproval,
19
16
  } from "../contracts/approvals";
17
+ import { approvalCard } from "./approval-card";
20
18
  import { approvalMessages } from "./messages";
21
19
 
22
20
  /** How a decided request ends in this list. It stays in place as a receipt until the next load. */
@@ -57,60 +55,13 @@ const useMessages = () => {
57
55
  return () => approvalMessages.resolve([locale()]).t;
58
56
  };
59
57
 
60
- const lines = (item: PendingApproval) =>
61
- item.sentence
62
- .split("\n")
63
- .map((line) => line.trim())
64
- .filter(Boolean);
65
-
66
- /** The first line names the app and the Action; the app already shows as the row's icon and origin. */
67
- const approvalTitle = (item: PendingApproval): string => {
68
- const first = lines(item)[0] ?? item.action;
69
- const prefix = `${item.app.name}: `;
70
- return first.startsWith(prefix) && first.length > prefix.length ? first.slice(prefix.length) : first;
71
- };
72
-
73
- /** Without a review, the sentence's later `Label: value` lines are what the person approves. */
74
- const sentenceRows = (item: PendingApproval) =>
75
- lines(item)
76
- .slice(1)
77
- .map((line) => {
78
- const match = /^([^:]{1,40}):\s+(.+)$/.exec(line);
79
- return match ? { label: match[1]!, value: match[2] ?? "" } : { label: "", value: line };
80
- });
81
-
82
- /**
83
- * The same card as the chat: a new review gives the app's sentence, reason, and preview; an old review its literal
84
- * message and rows; a request without a review its sentence. Times show in the reader's zone (UTC until the page
85
- * passes one, the same on the server and in the first browser render).
86
- */
87
- const approvalCard = (item: PendingApproval, locale: string, timeZone: string): ActionCard => {
88
- const app = { name: item.app.name, icon: item.app.icon ?? "ti ti-shield-check" };
89
- const context = { locale, timeZone, app };
90
- const review = item.review ? storedActionReview(item.review) : null;
91
- if (review && !isLegacyActionReview(review)) return reviewActionCard(review, context);
92
- const title = approvalTitle(item);
93
- const t = aiChatMessages(locale);
94
- return legacyActionCard(
95
- {
96
- question: review?.legacy.question ?? capabilityQuestionText(title, locale),
97
- rows: review?.legacy.rows ?? sentenceRows(item),
98
- ...(review?.legacy.markdown ? { markdown: review.legacy.markdown } : {}),
99
- ...(review?.legacy.links ? { links: review.legacy.links } : {}),
100
- subject: title,
101
- title,
102
- outcome: {
103
- running: t.receiptRunning({ title }),
104
- done: t.receiptDone({ title }),
105
- rejected: t.receiptRejected({ title }),
106
- notRun: t.receiptNotRun({ title, stopped: false }),
107
- unknown: capabilityUnknownText(title, locale),
108
- unknownStill: capabilityUnknownText(title, locale, true),
109
- },
110
- },
111
- context,
112
- );
113
- };
58
+ const cardInput = (item: PendingApproval) => ({
59
+ sentence: item.sentence,
60
+ action: item.action,
61
+ appName: item.app.name,
62
+ appIcon: item.app.icon,
63
+ review: item.review,
64
+ });
114
65
 
115
66
  /** Origin of the request: owning app, the chat it waits in, when Cloud asked, and when it expires. */
116
67
  function Origin(props: { item: PendingApproval; withApp?: boolean }) {
@@ -170,7 +121,7 @@ function ApprovalCard(props: {
170
121
  }) {
171
122
  const t = useMessages();
172
123
  const locale = useLocale();
173
- const card = createMemo(() => approvalCard(props.item, locale(), READER_TIME_ZONE));
124
+ const card = createMemo(() => approvalCard(cardInput(props.item), locale(), READER_TIME_ZONE));
174
125
  const current = () => props.outcome ?? (props.failed ? "failed" : "sending");
175
126
  const state = () => (props.outcome || props.busy || props.failed ? "decided" : "open");
176
127
  // `confirm` asks every time, whatever the stored options say.
@@ -228,9 +179,11 @@ function BandDecision(props: {
228
179
  <Button size="sm" variant="ghost" disabled={props.busy} onClick={() => props.decide({ approved: false })}>
229
180
  {t().reject}
230
181
  </Button>
231
- <Button size="sm" variant="primary" loading={props.busy} onClick={() => props.decide({ approved: true })}>
232
- {t().approve}
233
- </Button>
182
+ <Show when={!(props.item.review && "contentRef" in props.item.review && props.item.review.contentRef)}>
183
+ <Button size="sm" variant="primary" loading={props.busy} onClick={() => props.decide({ approved: true })}>
184
+ {t().approve}
185
+ </Button>
186
+ </Show>
234
187
  </div>
235
188
  );
236
189
  }
@@ -251,7 +204,7 @@ const openDetails = (item: PendingApproval, locale: string) =>
251
204
  dialogCore.open<ApprovalDecisionInput>(
252
205
  (close) => {
253
206
  const t = () => approvalMessages.resolve([locale]).t;
254
- const card = approvalCard(item, locale, READER_TIME_ZONE);
207
+ const card = approvalCard(cardInput(item), locale, READER_TIME_ZONE);
255
208
  return (
256
209
  <PanelDialog>
257
210
  <PanelDialog.Header
@@ -290,7 +243,13 @@ const openDetails = (item: PendingApproval, locale: string) =>
290
243
  </PanelDialog>
291
244
  );
292
245
  },
293
- { ...panelDialogOptions, history: true, ariaLabel: approvalMessages.resolve([locale]).t.approvalFor({ title: approvalTitle(item) }) },
246
+ {
247
+ ...panelDialogOptions,
248
+ history: true,
249
+ ariaLabel: approvalMessages
250
+ .resolve([locale])
251
+ .t.approvalFor({ title: approvalCard(cardInput(item), locale, READER_TIME_ZONE).questionText }),
252
+ },
294
253
  );
295
254
 
296
255
  function Row(props: {
@@ -304,6 +263,8 @@ function Row(props: {
304
263
  }) {
305
264
  const t = useMessages();
306
265
  const locale = useLocale();
266
+ // The band words the request as the card does: the plain question and Cloud's reason line.
267
+ const card = createMemo(() => approvalCard(cardInput(props.item), locale(), READER_TIME_ZONE));
307
268
  const details = async () => {
308
269
  const input = await openDetails(props.item, locale());
309
270
  if (input) props.decide(input);
@@ -322,10 +283,13 @@ function Row(props: {
322
283
  />
323
284
  }
324
285
  >
325
- <article class="cloud-approval-band" data-decided={props.outcome} aria-label={t().approvalFor({ title: approvalTitle(props.item) })}>
286
+ <article class="cloud-approval-band" data-decided={props.outcome} aria-label={t().approvalFor({ title: card().questionText })}>
326
287
  <i class={props.outcome ? outcomeIcon(props.outcome, true) : "ti ti-alert-triangle"} aria-hidden="true" />
327
288
  <div class="cloud-approval-band__text">
328
- <strong>{approvalTitle(props.item)}</strong>
289
+ <strong lang={card().locale}>{card().questionText}</strong>
290
+ <span class="cloud-approval-band__reason" lang={card().locale}>
291
+ {card().reason.text}
292
+ </span>
329
293
  <Origin item={props.item} withApp={false} />
330
294
  </div>
331
295
  <div class="cloud-approval__foot">
@@ -0,0 +1,68 @@
1
+ import { capabilityQuestionText, capabilityUnknownText } from "../_internal/capability-sentences";
2
+ import { type AiStoredActionReview, isLegacyActionReview, storedActionReview } from "../ai/action-review";
3
+ import { type ActionCard, legacyActionCard, reviewActionCard } from "../ai/chat/action-card";
4
+ import { aiChatMessages } from "../ai/chat/messages";
5
+ import type { CapabilityActionReviewData } from "../contracts/capabilities";
6
+
7
+ export type ApprovalCardInput = {
8
+ sentence: string;
9
+ action: string;
10
+ appName: string;
11
+ appIcon?: string | null;
12
+ review: CapabilityActionReviewData | AiStoredActionReview | null;
13
+ };
14
+
15
+ const lines = (item: ApprovalCardInput) =>
16
+ item.sentence
17
+ .split("\n")
18
+ .map((line) => line.trim())
19
+ .filter(Boolean);
20
+
21
+ /** The first line names the app and the Action; the app already shows as the row's icon and origin. */
22
+ const approvalTitle = (item: ApprovalCardInput): string => {
23
+ const first = lines(item)[0] ?? item.action;
24
+ const prefix = `${item.appName}: `;
25
+ return first.startsWith(prefix) && first.length > prefix.length ? first.slice(prefix.length) : first;
26
+ };
27
+
28
+ /** Without a review, the sentence's later `Label: value` lines are what the person approves. */
29
+ const sentenceRows = (item: ApprovalCardInput) =>
30
+ lines(item)
31
+ .slice(1)
32
+ .map((line) => {
33
+ const match = /^([^:]{1,40}):\s+(\S.*)$/.exec(line);
34
+ return match ? { label: match[1]!, value: match[2] ?? "" } : { label: "", value: line };
35
+ });
36
+
37
+ /**
38
+ * The same card as the chat: a new review gives the app's sentence, reason, and preview; an old review its literal
39
+ * message and rows; a request without a review its sentence. Times show in the reader's zone (UTC until the page
40
+ * passes one, the same on the server and in the first browser render).
41
+ */
42
+ export const approvalCard = (item: ApprovalCardInput, locale: string, timeZone: string): ActionCard => {
43
+ const app = { name: item.appName, icon: item.appIcon ?? "ti ti-shield-check" };
44
+ const context = { locale, timeZone, app };
45
+ const review = item.review ? ("legacy" in item.review ? item.review : storedActionReview(item.review)) : null;
46
+ if (review && !isLegacyActionReview(review)) return reviewActionCard(review, context);
47
+ const title = approvalTitle(item);
48
+ const t = aiChatMessages(locale);
49
+ return legacyActionCard(
50
+ {
51
+ question: review?.legacy.question ?? capabilityQuestionText(title, locale),
52
+ rows: review?.legacy.rows ?? sentenceRows(item),
53
+ ...(review?.legacy.markdown ? { markdown: review.legacy.markdown } : {}),
54
+ ...(review?.legacy.links ? { links: review.legacy.links } : {}),
55
+ subject: title,
56
+ title,
57
+ outcome: {
58
+ running: t.receiptRunning({ title }),
59
+ done: t.receiptDone({ title }),
60
+ rejected: t.receiptRejected({ title }),
61
+ notRun: t.receiptNotRun({ title, stopped: false }),
62
+ unknown: capabilityUnknownText(title, locale),
63
+ unknownStill: capabilityUnknownText(title, locale, true),
64
+ },
65
+ },
66
+ context,
67
+ );
68
+ };
@@ -1,6 +1,12 @@
1
+ import { LocaleProvider } from "@k2b/ui";
2
+ import { render } from "solid-js/web";
1
3
  import { appAccentStyle } from "../ssr/app-appearance";
2
4
  import { createGlobalSearchHost } from "../ssr/global-search-host";
5
+ import { platformMessages } from "../ssr/platform-messages";
6
+ import ShellRuntime from "../ssr/ShellRuntime.island";
7
+ import type { ShellData } from "../ssr/shell-data";
3
8
  import { openCloudResourcePicker } from "./resource-picker";
9
+ import { openGlobalSearch } from "./search";
4
10
 
5
11
  /**
6
12
  * Opens the real global search, or with `?picker` the resource picker, for a browser test that answers
@@ -8,7 +14,53 @@ import { openCloudResourcePicker } from "./resource-picker";
8
14
  */
9
15
  const params = new URLSearchParams(location.search);
10
16
  const app = params.get("app") ?? undefined;
11
- if (params.has("picker")) {
17
+ if (params.has("launcher")) {
18
+ // Invented authorized shell data, rendered by the real ShellRuntime and global-search host.
19
+ // Role filtering itself is covered at Layout's SSR seam; this harness does not simulate authentication.
20
+ const locale = document.documentElement.lang || "en";
21
+ const de = locale === "de";
22
+ const data: ShellData = {
23
+ apps: [
24
+ { id: "tools", label: de ? "Werkzeuge" : "Tools", iconClass: "ti ti-tools", href: "/tools", match: "/tools", defaultVisible: false },
25
+ ...(params.has("admin")
26
+ ? [
27
+ {
28
+ id: "admin",
29
+ label: platformMessages.resolve([locale]).t.admin,
30
+ iconClass: "ti ti-settings",
31
+ href: "/admin",
32
+ match: "/admin",
33
+ defaultVisible: false,
34
+ },
35
+ ]
36
+ : []),
37
+ ],
38
+ searchLinks: {
39
+ tools: [
40
+ { label: "Tools", href: "/tools", keywords: ["utilities", "werkzeuge"] },
41
+ { label: "QR Code", href: "/tools/qr", keywords: ["qr"] },
42
+ ],
43
+ },
44
+ legalLinks: [],
45
+ theme: "light",
46
+ };
47
+ const script = document.createElement("script");
48
+ script.id = "cloud-shell-data";
49
+ script.type = "application/json";
50
+ script.textContent = JSON.stringify(data);
51
+ document.body.append(script);
52
+ const host = document.createElement("div");
53
+ document.body.append(host);
54
+ render(
55
+ () => (
56
+ <LocaleProvider locale={locale}>
57
+ <ShellRuntime commands={{ searchResources: !params.has("public") }} />
58
+ </LocaleProvider>
59
+ ),
60
+ host,
61
+ );
62
+ openGlobalSearch(app ? { scope: { appId: app, label: app } } : { query: params.get("q") ?? "" });
63
+ } else if (params.has("picker")) {
12
64
  // The page shell renders `<body class="k2b-ui app-accent-scope" style="…">` for an app page.
13
65
  const style = appAccentStyle(params.get("accent") ?? undefined);
14
66
  if (style) {
@@ -51,15 +51,6 @@ const isLinkableCommand = (command: PaletteCommand) => typeof command.action !==
51
51
 
52
52
  const itemKey = (item: SearchItem) => `${item.ref.type}:${item.ref.id}`;
53
53
  const URL_BASE = "https://cloud.invalid";
54
- const groupByApp = (items: SearchItem[]) => {
55
- const groups = new Map<string, SearchItem[]>();
56
- for (const item of items) {
57
- const group = groups.get(item.appId) ?? [];
58
- group.push(item);
59
- groups.set(item.appId, group);
60
- }
61
- return [...groups.values()].flat();
62
- };
63
54
 
64
55
  export default function CloudResourceSearch(props: CloudResourceSearchProps) {
65
56
  const locale = useLocale();
@@ -236,9 +227,7 @@ export default function CloudResourceSearch(props: CloudResourceSearchProps) {
236
227
  /** Rows of the run for the current input; anything older is visible but cannot be chosen. */
237
228
  const freshResults = () => current() && shownRun();
238
229
  const results = createMemo(() =>
239
- canSearch()
240
- ? [...groupByApp(navigation()), ...shownBlocks().flatMap((block) => filterCloudResourceSearchItems(block.items, props))]
241
- : [],
230
+ canSearch() ? [...navigation(), ...shownBlocks().flatMap((block) => filterCloudResourceSearchItems(block.items, props))] : [],
242
231
  );
243
232
  const searchContext = () => JSON.stringify([desiredUrl(), commandMode(), commandQuery()]);
244
233
  /**
@@ -460,6 +449,7 @@ export default function CloudResourceSearch(props: CloudResourceSearchProps) {
460
449
  }
461
450
  };
462
451
  const updateCaret = () => setCaret(inputRef.selectionStart ?? input().length);
452
+ const isApp = (item: SearchItem) => navigation().some((link) => link === item && link.kind === "app");
463
453
  const previewMetadata = () => (previewItem()?.metadata ?? []).filter((entry) => entry.label.trim() && entry.value.trim()).slice(0, 5);
464
454
 
465
455
  return (
@@ -670,7 +660,7 @@ export default function CloudResourceSearch(props: CloudResourceSearchProps) {
670
660
  <i class={item.icon ?? item.appIcon} aria-hidden="true" />
671
661
  <span class="cloud-resource-search__result-copy">
672
662
  <span>{item.title}</span>
673
- <small>{item.preview ?? item.appName}</small>
663
+ <small>{isApp(item) ? t().app : (item.preview ?? item.appName)}</small>
674
664
  </span>
675
665
  <Show when={commandFor(item)?.shortcut}>{(shortcut) => <Kbd keys={shortcut()} />}</Show>
676
666
  <Show when={props.selectionMode && selectedKey() === itemKey(item)}>
@@ -0,0 +1,18 @@
1
+ import { Button, NoticeCard } from "@k2b/ui";
2
+ import { Show } from "solid-js";
3
+ import { platformMessages } from "../ssr/platform-messages";
4
+
5
+ export default function PanelLeaveNotice(props: { blocked: boolean; locale: string; resume: () => void }) {
6
+ const t = () => platformMessages.resolve([props.locale]).t;
7
+ return (
8
+ <Show when={props.blocked}>
9
+ <div role="status" data-cloud-panel-leave="blocked">
10
+ <NoticeCard tone="warning" title={t().panelLeaveBlocked} detail={t().panelLeaveDetail}>
11
+ <Button variant="ghost" onClick={props.resume}>
12
+ {t().panelLeaveResume}
13
+ </Button>
14
+ </NoticeCard>
15
+ </div>
16
+ </Show>
17
+ );
18
+ }