@k2b/cloud 0.6.0 → 0.7.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 (309) hide show
  1. package/README.md +2 -2
  2. package/package.json +19 -9
  3. package/scripts/browser-performance.ts +14 -0
  4. package/scripts/build-pdf-renderer.ts +39 -0
  5. package/scripts/build.ts +15 -35
  6. package/scripts/preload.ts +9 -12
  7. package/scripts/sync-recovery-smoke.ts +2 -1
  8. package/src/_internal/app-presentation.ts +14 -2
  9. package/src/_internal/capabilities.ts +108 -7
  10. package/src/_internal/capability-streams.ts +81 -0
  11. package/src/_internal/define-app.ts +62 -36
  12. package/src/_internal/heartbeat.ts +4 -0
  13. package/src/_internal/help-catalog.ts +27 -172
  14. package/src/_internal/help.ts +13 -76
  15. package/src/_internal/page-responses.ts +2 -0
  16. package/src/_internal/postgres-application-name.ts +30 -0
  17. package/src/_internal/process-identity.ts +15 -0
  18. package/src/_internal/process-sync.ts +1 -1
  19. package/src/_internal/registry-validation.ts +29 -25
  20. package/src/_internal/registry.ts +2 -53
  21. package/src/_internal/runtime-context.ts +5 -16
  22. package/src/_internal/server-timing.ts +13 -0
  23. package/src/_internal/status-preserving-ssr.ts +18 -6
  24. package/src/_internal/web-vitals-asset.ts +10 -0
  25. package/src/access/PermissionEditor.tsx +14 -153
  26. package/src/access/PrincipalPicker.tsx +104 -0
  27. package/src/access/ui.ts +2 -0
  28. package/src/ai/admin.ts +14 -13
  29. package/src/ai/assistant-models.ts +12 -1
  30. package/src/ai/audio-format.ts +34 -0
  31. package/src/ai/audio-tool.ts +90 -0
  32. package/src/ai/browser-code-contracts.ts +79 -0
  33. package/src/ai/browser.ts +2 -0
  34. package/src/ai/capabilities.ts +55 -36
  35. package/src/ai/capability-execution.ts +47 -12
  36. package/src/ai/chat/blocks.tsx +157 -95
  37. package/src/ai/chat/capability-result.ts +15 -0
  38. package/src/ai/chat/capability-table.tsx +2 -10
  39. package/src/ai/chat/composer-adapter.ts +48 -3
  40. package/src/ai/chat/message-actions.tsx +8 -7
  41. package/src/ai/chat/message-utils.ts +12 -1
  42. package/src/ai/chat/messages.ts +2 -0
  43. package/src/ai/chat/presentation.tsx +19 -48
  44. package/src/ai/chat/primitives.tsx +43 -1
  45. package/src/ai/chat/tool-disclosure.tsx +17 -2
  46. package/src/ai/chat/tool-groups.ts +52 -0
  47. package/src/ai/chat-quotas.ts +25 -0
  48. package/src/ai/chat-task-contracts.ts +9 -1
  49. package/src/ai/chat-tasks.ts +276 -43
  50. package/src/ai/client/controller.ts +110 -33
  51. package/src/ai/client/file-source.ts +12 -6
  52. package/src/ai/client/live-connection.ts +1 -0
  53. package/src/ai/client/projection.ts +22 -11
  54. package/src/ai/client/transport.ts +4 -0
  55. package/src/ai/code-capability-routes.ts +110 -0
  56. package/src/ai/code-capability-transport.ts +38 -0
  57. package/src/ai/code-mode-skill.ts +114 -0
  58. package/src/ai/code-runtime-tools.ts +166 -0
  59. package/src/ai/code-source-contracts.ts +202 -0
  60. package/src/ai/code-source-tools.ts +145 -0
  61. package/src/ai/compaction.ts +27 -6
  62. package/src/ai/data-analysis-skill.ts +10 -0
  63. package/src/ai/default-tools.ts +25 -2
  64. package/src/ai/dictation-runtime.ts +171 -0
  65. package/src/ai/dictations.ts +188 -0
  66. package/src/ai/draft-content.ts +34 -0
  67. package/src/ai/enrich.ts +6 -0
  68. package/src/ai/executor.ts +191 -31
  69. package/src/ai/file-content-version.ts +22 -0
  70. package/src/ai/file-context.ts +2 -1
  71. package/src/ai/file-media-type.ts +12 -0
  72. package/src/ai/file-reference-contracts.ts +15 -0
  73. package/src/ai/file-tools.ts +2 -1
  74. package/src/ai/files-store.ts +174 -71
  75. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  76. package/src/ai/grids-skill.ts +40 -10
  77. package/src/ai/http.ts +15 -8
  78. package/src/ai/index.ts +26 -2
  79. package/src/ai/inference-calls.ts +198 -0
  80. package/src/ai/live-events.ts +5 -1
  81. package/src/ai/memory-learning.ts +60 -28
  82. package/src/ai/message-queue.ts +167 -0
  83. package/src/ai/migrate.ts +187 -11
  84. package/src/ai/model-pricing.ts +43 -0
  85. package/src/ai/pdf-render-worker.ts +74 -0
  86. package/src/ai/pdf-render.fixture.ts +28 -0
  87. package/src/ai/pdf-render.ts +77 -0
  88. package/src/ai/projects-routes.ts +3 -3
  89. package/src/ai/projects.ts +20 -6
  90. package/src/ai/protocol.ts +3 -0
  91. package/src/ai/provider.ts +1 -8
  92. package/src/ai/quota-provider.ts +191 -0
  93. package/src/ai/quota-report.ts +152 -0
  94. package/src/ai/quotas-migrate.ts +45 -0
  95. package/src/ai/quotas.ts +168 -0
  96. package/src/ai/routes.ts +215 -11
  97. package/src/ai/run-timeout.ts +10 -0
  98. package/src/ai/runtime-tools.ts +3 -0
  99. package/src/ai/runtime.ts +55 -16
  100. package/src/ai/settings.ts +36 -8
  101. package/src/ai/short-id.ts +2 -2
  102. package/src/ai/skill-catalog.ts +0 -10
  103. package/src/ai/skill-search.ts +47 -0
  104. package/src/ai/skill-seeds.ts +105 -20
  105. package/src/ai/skill-tool.ts +16 -5
  106. package/src/ai/skills-routes.ts +32 -4
  107. package/src/ai/skills.ts +324 -155
  108. package/src/ai/store.ts +373 -191
  109. package/src/ai/stream.ts +5 -0
  110. package/src/ai/structured-runs.ts +4 -3
  111. package/src/ai/structured.ts +8 -1
  112. package/src/ai/system-prompt.ts +12 -2
  113. package/src/ai/timeline.ts +2 -5
  114. package/src/ai/todo-contracts.ts +20 -0
  115. package/src/ai/todo-tool.ts +16 -0
  116. package/src/ai/tool-audit.ts +30 -40
  117. package/src/ai/tools.ts +6 -0
  118. package/src/ai/transcription.ts +202 -0
  119. package/src/ai/turn-timing.ts +114 -0
  120. package/src/ai/types.ts +58 -10
  121. package/src/ai/ui.tsx +2 -0
  122. package/src/ai/usage.ts +118 -118
  123. package/src/ai/vision-tool.ts +39 -10
  124. package/src/api/admin-ai-projects.ts +2 -2
  125. package/src/api/admin-ai-quotas.ts +74 -0
  126. package/src/api/admin-ai-skills.ts +48 -5
  127. package/src/api/admin-ai-usage.ts +2 -2
  128. package/src/api/admin-core-settings.ts +25 -0
  129. package/src/api/admin-identity.ts +91 -51
  130. package/src/api/admin-rail.ts +58 -0
  131. package/src/api/announcements.ts +14 -0
  132. package/src/api/capabilities.ts +292 -43
  133. package/src/api/capability-streams.ts +262 -0
  134. package/src/api/help.ts +8 -44
  135. package/src/api/index.ts +6 -0
  136. package/src/api/mcp.ts +44 -26
  137. package/src/api/me-web-vitals.ts +35 -0
  138. package/src/api/me.ts +2 -0
  139. package/src/api/search/schemas.ts +26 -15
  140. package/src/api/search.bench.ts +1 -1
  141. package/src/api/search.ts +54 -17
  142. package/src/browser/CloudResourceSearch.tsx +640 -702
  143. package/src/browser/app-approval-vault.ts +19 -7
  144. package/src/browser/command-bridge.ts +109 -0
  145. package/src/browser/command-shortcuts.ts +76 -0
  146. package/src/browser/commands.ts +66 -0
  147. package/src/browser/navigation-search.ts +48 -0
  148. package/src/browser/notifications.ts +1 -1
  149. package/src/browser/resource-picker.tsx +5 -18
  150. package/src/browser/resource-search-dialog.ts +9 -0
  151. package/src/browser/resource-search-input.ts +46 -0
  152. package/src/browser/resource-search-messages.ts +76 -40
  153. package/src/browser/resource-search.ts +7 -0
  154. package/src/browser/search-bridge.ts +102 -0
  155. package/src/browser/search-commands.ts +59 -0
  156. package/src/browser/search.ts +13 -0
  157. package/src/browser/spotlight-position.ts +208 -0
  158. package/src/browser/testing.ts +5 -0
  159. package/src/browser/web-vitals.ts +25 -0
  160. package/src/capabilities/claims.ts +179 -0
  161. package/src/capabilities/client.ts +34 -0
  162. package/src/capabilities/command-link.ts +16 -0
  163. package/src/capabilities/executions.ts +298 -0
  164. package/src/capabilities/migrate.ts +81 -0
  165. package/src/capabilities/server.ts +114 -34
  166. package/src/capabilities/stream-body.ts +16 -0
  167. package/src/capabilities/streams.ts +28 -0
  168. package/src/cli/admin/ai-quotas.ts +161 -0
  169. package/src/cli/admin/ai-skills.ts +64 -0
  170. package/src/cli/admin/ai-usage.ts +53 -30
  171. package/src/cli/admin/app-credentials.ts +79 -0
  172. package/src/cli/admin/data.ts +17 -0
  173. package/src/cli/admin/index.ts +9 -0
  174. package/src/cli/capabilities.ts +29 -0
  175. package/src/cli/capability-streams.ts +25 -0
  176. package/src/cli/index.ts +3 -2
  177. package/src/config/env.ts +11 -8
  178. package/src/contracts/app.ts +23 -3
  179. package/src/contracts/capabilities.ts +68 -5
  180. package/src/contracts/capability-streams.ts +46 -0
  181. package/src/contracts/commands.ts +49 -0
  182. package/src/contracts/index.ts +6 -1
  183. package/src/contracts/rail-admin.ts +38 -0
  184. package/src/contracts/rail-preferences.ts +16 -0
  185. package/src/contracts/registry.ts +3 -27
  186. package/src/contracts/settings-types.ts +1 -1
  187. package/src/contracts/shared.ts +9 -5
  188. package/src/contracts/web-vitals.ts +27 -0
  189. package/src/index.ts +0 -4
  190. package/src/server/index.ts +3 -0
  191. package/src/server/middleware/auth.ts +3 -2
  192. package/src/server/middleware/route-template.ts +9 -3
  193. package/src/server/middleware/runtime.ts +2 -1
  194. package/src/server/middleware/settings.ts +17 -11
  195. package/src/server/services/access-revision.ts +10 -0
  196. package/src/server/services/access.ts +1 -1
  197. package/src/server/services/index.ts +1 -0
  198. package/src/services/account-category-policy.ts +9 -0
  199. package/src/services/account-lifecycle/audit.ts +1 -1
  200. package/src/services/accounts/app.ts +12 -2
  201. package/src/services/accounts/email-write.ts +23 -0
  202. package/src/services/accounts/groups.ts +15 -4
  203. package/src/services/accounts/identities.ts +249 -0
  204. package/src/services/accounts/identity-reconciliation.ts +87 -0
  205. package/src/services/accounts/ipa-data.ts +2 -2
  206. package/src/services/accounts/local-groups.ts +2 -2
  207. package/src/services/accounts/posix.ts +45 -29
  208. package/src/services/accounts/users.ts +2 -2
  209. package/src/services/announcements/index.ts +69 -8
  210. package/src/services/audit/index.ts +1 -1
  211. package/src/services/cache-fill.ts +35 -0
  212. package/src/services/help/index.ts +109 -0
  213. package/src/services/help/maintenance.ts +25 -0
  214. package/src/services/help/store.ts +180 -0
  215. package/src/services/help/types.ts +46 -0
  216. package/src/services/identity/invocation-actor.ts +2 -2
  217. package/src/services/identity/key-config.ts +2 -1
  218. package/src/services/identity/key-ring.ts +1 -1
  219. package/src/services/index.ts +15 -0
  220. package/src/services/ipa/users.ts +268 -221
  221. package/src/services/legal-consent.ts +1 -1
  222. package/src/services/logging/index.ts +15 -13
  223. package/src/services/logging/trace.ts +2 -1
  224. package/src/services/logging/web-vitals.ts +81 -0
  225. package/src/services/mandates/index.ts +18 -4
  226. package/src/services/mandates/policy.ts +65 -2
  227. package/src/services/notifications/batches.ts +124 -57
  228. package/src/services/notifications/browser.ts +12 -0
  229. package/src/services/notifications/index.ts +0 -2
  230. package/src/services/notifications/platform.ts +0 -4
  231. package/src/services/oauth-tokens.ts +3 -3
  232. package/src/services/pdf/gotenberg.ts +107 -84
  233. package/src/services/pdf/index.ts +3 -0
  234. package/src/services/providers/local/users.ts +25 -16
  235. package/src/services/public-http.ts +101 -0
  236. package/src/services/rail-shortcuts.ts +95 -0
  237. package/src/services/rail-snapshot.ts +53 -0
  238. package/src/services/request-cache-redis.ts +28 -0
  239. package/src/services/session/index.ts +15 -2
  240. package/src/services/session/user.ts +62 -37
  241. package/src/services/settings/core-settings.ts +21 -28
  242. package/src/services/settings/defaults.ts +5 -0
  243. package/src/services/settings/store.ts +58 -84
  244. package/src/shared/ai-costs.ts +42 -0
  245. package/src/shared/ai-platform-prompt.ts +1 -1
  246. package/src/shared/ai-quotas.ts +136 -0
  247. package/src/shared/ai-usage.ts +6 -16
  248. package/src/shared/app-presentation.ts +21 -15
  249. package/src/shared/capability-messages.ts +14 -0
  250. package/src/shared/help.ts +6 -42
  251. package/src/shared/index.ts +6 -0
  252. package/src/ssr/AdminLayout.tsx +7 -5
  253. package/src/ssr/AdminSidebar.tsx +29 -15
  254. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  255. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  256. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  257. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  258. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  259. package/src/ssr/Layout.tsx +17 -6
  260. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  261. package/src/ssr/LayoutFooter.tsx +1 -1
  262. package/src/ssr/LayoutHeader.tsx +46 -14
  263. package/src/ssr/LayoutHelp.tsx +23 -22
  264. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  265. package/src/ssr/LayoutRail.tsx +17 -9
  266. package/src/ssr/MobileNavigation.tsx +71 -0
  267. package/src/ssr/MobileProfileActions.tsx +49 -0
  268. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  269. package/src/ssr/RailApps.island.tsx +2 -2
  270. package/src/ssr/RailEditor.tsx +17 -2
  271. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  272. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  273. package/src/ssr/admin-navigation.ts +7 -1
  274. package/src/ssr/help.ts +29 -0
  275. package/src/ssr/islands/SearchBar.tsx +67 -0
  276. package/src/ssr/islands/index.ts +3 -1
  277. package/src/ssr/layout-context.ts +2 -2
  278. package/src/ssr/layout-help-search.ts +15 -0
  279. package/src/ssr/mobile-menu-history.ts +70 -0
  280. package/src/ssr/platform-messages.ts +16 -8
  281. package/src/ssr/profile-actions.ts +42 -0
  282. package/src/ssr/rail-context.ts +8 -4
  283. package/src/ssr/rail-messages.ts +2 -0
  284. package/src/ssr/rail-navigation.ts +3 -3
  285. package/src/ssr/workspace-navigation.ts +75 -0
  286. package/src/styles/effects.css +6 -6
  287. package/src/styles/global.css +54 -0
  288. package/src/styles/input.css +43 -41
  289. package/src/styles/resource-search.css +505 -0
  290. package/src/styles/utilities-feedback.css +13 -0
  291. package/src/styles/utilities-layout.css +1 -5
  292. package/src/styles/utilities-navigation.css +3 -39
  293. package/src/types/ambient.d.ts +5 -0
  294. package/src/workflows/ai/runtime.ts +5 -1
  295. package/src/workflows/ai/store.ts +2 -0
  296. package/src/workflows/ai-actions.ts +16 -13
  297. package/src/workflows/definition.ts +10 -5
  298. package/src/workflows/store/actions.ts +44 -20
  299. package/src/workflows/store/index.ts +2 -0
  300. package/src/workflows/store/runs.ts +36 -0
  301. package/src/workflows/store/worker-pool.ts +55 -0
  302. package/src/workflows/store/worker-runtime.ts +90 -0
  303. package/src/ai/kit-skill.ts +0 -22
  304. package/src/ai/usage-migrate.ts +0 -81
  305. package/src/browser/notification-target.ts +0 -27
  306. package/src/contracts/notification-live.ts +0 -66
  307. package/src/services/notifications/live.ts +0 -50
  308. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  309. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -1,13 +1,10 @@
1
+ import { createHelpReader, type HelpReaderFactory } from "../services/help";
2
+ import { createCodeSourceTool } from "./code-source-tools";
3
+ import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
1
4
  import { createHash } from "node:crypto";
2
5
  import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
3
6
  import { z } from "zod";
4
- import {
5
- createHelpCatalog,
6
- HELP_READ_MAX_CHARS,
7
- HELP_SEARCH_MAX_LIMIT,
8
- readHelpCatalog,
9
- searchHelpCatalog,
10
- } from "../_internal/help-catalog";
7
+ import { HELP_READ_MAX_CHARS, HELP_SEARCH_MAX_LIMIT, readHelpArticle } from "../_internal/help-catalog";
11
8
  import {
12
9
  type CapabilityActionManifest,
13
10
  type CapabilityActionReview,
@@ -16,8 +13,9 @@ import {
16
13
  cloudResourceRefAppId,
17
14
  resolveCapabilityResourceReader,
18
15
  } from "../contracts/capabilities";
19
- import type { CapabilityRegistryEntry, HelpRegistryEntry } from "../contracts/registry";
16
+ import type { CapabilityRegistryEntry } from "../contracts/registry";
20
17
  import type { RequestActor } from "../server";
18
+ import { recordRejectedAiCapability } from "./capability-execution";
21
19
  import { CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES } from "./default-tools";
22
20
  import { type AiToolPreparationContext, defineAiTool, type PreparedAiTools, prepareAiTools } from "./tools";
23
21
  import type { AiConversationService, AiRuntimeTool, AiToolPresentation } from "./types";
@@ -304,14 +302,14 @@ const AiHelpDocumentSchema = AiHelpCatalogItemSchema.extend({
304
302
  truncated: z.boolean(),
305
303
  }).strict();
306
304
 
307
- /** Search and read the live Help snapshot without loading one tool per article. */
308
- export const createAiHelpTools = (registry: readonly HelpRegistryEntry[], locale = "en"): AiRuntimeTool[] => {
309
- const documents = createHelpCatalog(registry, locale);
305
+ /** Search and read published Help without loading one tool per article. */
306
+ export const createAiHelpTools = (help: HelpReaderFactory = createHelpReader, locale = "en"): AiRuntimeTool[] => {
307
+ const reader = help(locale);
310
308
 
311
309
  const search = defineAiTool({
312
310
  name: "search_help",
313
311
  description:
314
- "Search installed Cloud app Help when product behavior, settings, workflows, permissions, or app errors are unclear. Use 1-3 concise English product terms and scope appId when known. Returns compact document ids for read_help; skip this tool for straightforward live-data requests.",
312
+ "Search installed Cloud app Help when product behavior, settings, workflows, permissions, or app errors are unclear. Use concise product terms in the request language and scope appId when known. Returns compact document ids for read_help; skip this tool for straightforward live-data requests.",
315
313
  inputSchema: z
316
314
  .object({
317
315
  query: z.string().trim().min(1).max(200).describe("Product task or concept to find."),
@@ -322,7 +320,7 @@ export const createAiHelpTools = (registry: readonly HelpRegistryEntry[], locale
322
320
  outputSchema: z.object({ documents: z.array(AiHelpCatalogItemSchema).max(HELP_SEARCH_MAX_LIMIT) }).strict(),
323
321
  approval: "never",
324
322
  }).server(async ({ query, appId, limit }) => ({
325
- documents: searchHelpCatalog(documents, { query, appId, limit: boundedLimit(limit, DEFAULT_SEARCH_LIMIT, HELP_SEARCH_MAX_LIMIT) }),
323
+ documents: await reader.search({ query, appId, limit: boundedLimit(limit, DEFAULT_SEARCH_LIMIT, HELP_SEARCH_MAX_LIMIT) }),
326
324
  }));
327
325
 
328
326
  const read = defineAiTool({
@@ -338,25 +336,14 @@ export const createAiHelpTools = (registry: readonly HelpRegistryEntry[], locale
338
336
  .strict(),
339
337
  outputSchema: z.object({ document: AiHelpDocumentSchema.nullable() }).strict(),
340
338
  approval: "never",
341
- }).server(async ({ appId, documentId, query }) => ({
342
- document: readHelpCatalog(documents, { appId, documentId, query }),
343
- }));
339
+ }).server(async ({ appId, documentId, query }) => {
340
+ const document = await reader.read({ appId, documentId });
341
+ return { document: document ? readHelpArticle(document, query) : null };
342
+ });
344
343
 
345
344
  return [search, read];
346
345
  };
347
346
 
348
- const resolveHelpRegistry = async (
349
- listRegistry: () => Promise<HelpRegistryEntry[]>,
350
- onError?: (error: unknown) => void,
351
- ): Promise<HelpRegistryEntry[]> => {
352
- try {
353
- return await listRegistry();
354
- } catch (error) {
355
- onError?.(error);
356
- return [];
357
- }
358
- };
359
-
360
347
  const resolveCapabilityRegistry = async (
361
348
  listRegistry: () => Promise<CapabilityRegistryEntry[]>,
362
349
  onError?: (error: unknown) => void,
@@ -554,7 +541,9 @@ export const createAiToolMetaTools = (input: {
554
541
  export const createLoadedAiCapabilityTools = (input: {
555
542
  catalog: readonly AiCapabilityCatalogEntry[];
556
543
  loadedNames: readonly string[];
544
+ actor: RequestActor;
557
545
  review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<CapabilityActionReview | null>;
546
+ authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown) => Promise<void>;
558
547
  onReview?: (callId: string, review: CapabilityActionReview) => void;
559
548
  execute: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<unknown>;
560
549
  }): AiRuntimeTool[] => {
@@ -573,12 +562,18 @@ export const createLoadedAiCapabilityTools = (input: {
573
562
  // live review has resolved. The review may supply an app-owned scope.
574
563
  approval: "never",
575
564
  }).server(async (args, context) => {
576
- if (entry.kind === "action") {
565
+ await input.authorizeBackground?.(entry, args);
566
+ if (!input.authorizeBackground && entry.kind === "action" && (entry.operation as CapabilityActionManifest).approval !== "none") {
577
567
  const review = (await input.review?.(entry, args, context)) ?? null;
578
568
  if (review && context.callId) input.onReview?.(context.callId, review);
579
569
  const message =
580
570
  review?.message ?? `${entry.appName}: ${entry.title}\nReview the validated arguments below before running this Action.`;
581
- if (!(await context.requestApproval(message))) throw new Error("Capability Action was rejected by the user.");
571
+ if (!(await context.requestApproval(message))) {
572
+ if (input.actor.kind === "user") {
573
+ await recordRejectedAiCapability({ entry, actor: input.actor, args }).catch(() => undefined);
574
+ }
575
+ throw new Error("Capability Action was rejected by the user.");
576
+ }
582
577
  }
583
578
  return input.execute(entry, args, context);
584
579
  }),
@@ -599,6 +594,11 @@ export const createAiResourceReaderTool = (input: {
599
594
  outputSchema: z.unknown(),
600
595
  approval: "never",
601
596
  }).server(async (ref, context) => {
597
+ if (ref.type === "assistant.artifact") {
598
+ const tool = createCodeSourceTool("code_read");
599
+ if (tool.location !== "server") throw new Error("Code reader must execute on the server.");
600
+ return tool.run(CODE_SOURCE_TOOLS.code_read.input.parse({ id: ref.id }), context);
601
+ }
602
602
  const appId = cloudResourceRefAppId(ref);
603
603
  const app = input.apps.find((candidate) => candidate.appId === appId);
604
604
  const reader = app ? resolveCapabilityResourceReader(app.manifest, ref) : null;
@@ -611,6 +611,24 @@ export const createAiResourceReaderTool = (input: {
611
611
  });
612
612
 
613
613
  /** Nessi resolver: one registry/load-state snapshot drives discovery, loading, schemas, and execution per model turn. */
614
+ /** Background discovery belongs to one run, never the interactive conversation. */
615
+ export const createRunToolStore = (initialNames: readonly string[]): Pick<AiConversationService, "getLoadedTools" | "loadTools"> => {
616
+ let current = [...new Set(initialNames)];
617
+ return {
618
+ getLoadedTools: async () => [...current],
619
+ loadTools: async ({ names, maxLoadedTools }) => {
620
+ const requested = [...new Set(names.map((name) => name.trim()).filter(Boolean))];
621
+ const alreadyLoaded = requested.filter((name) => current.includes(name));
622
+ const added = requested.filter((name) => !current.includes(name));
623
+ const combined = [...current, ...added];
624
+ const limit = Math.floor(maxLoadedTools ?? 0);
625
+ const evicted = limit > 0 ? combined.slice(0, Math.max(0, combined.length - limit)) : [];
626
+ current = combined.slice(evicted.length);
627
+ return { loaded: added.filter((name) => current.includes(name)), alreadyLoaded, evicted };
628
+ },
629
+ };
630
+ };
631
+
614
632
  export const createAiToolResolver =
615
633
  (input: {
616
634
  conversationId: string;
@@ -621,12 +639,12 @@ export const createAiToolResolver =
621
639
  store: Pick<AiConversationService, "getLoadedTools" | "loadTools">;
622
640
  listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
623
641
  onCapabilityRegistryError?: (error: unknown) => void;
624
- listHelpRegistry?: () => Promise<HelpRegistryEntry[]>;
625
- onHelpRegistryError?: (error: unknown) => void;
642
+ help?: HelpReaderFactory;
626
643
  locale?: string;
627
644
  maxLoadedTools?: number;
628
645
  execute?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<unknown>;
629
646
  review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<CapabilityActionReview | null>;
647
+ authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown) => Promise<void>;
630
648
  onReview?: (callId: string, review: CapabilityActionReview) => void;
631
649
  onPrepared?: (snapshot: {
632
650
  prepared: PreparedAiTools;
@@ -635,10 +653,9 @@ export const createAiToolResolver =
635
653
  }) => void;
636
654
  }): ToolResolver =>
637
655
  async (): Promise<Tool[]> => {
638
- const [registry, persistedLoadedNames, helpRegistry] = await Promise.all([
656
+ const [registry, persistedLoadedNames] = await Promise.all([
639
657
  input.listRegistry ? resolveCapabilityRegistry(input.listRegistry, input.onCapabilityRegistryError) : [],
640
658
  input.store.getLoadedTools({ conversationId: input.conversationId }),
641
- input.listHelpRegistry ? resolveHelpRegistry(input.listHelpRegistry, input.onHelpRegistryError) : [],
642
659
  ]);
643
660
  const configuredLimit = Math.floor(input.maxLoadedTools ?? 0);
644
661
  const loadedNames = configuredLimit > 0 ? persistedLoadedNames.slice(-configuredLimit) : persistedLoadedNames;
@@ -651,9 +668,9 @@ export const createAiToolResolver =
651
668
  }
652
669
  const allowed = input.allowedTools == null ? null : new Set(input.allowedTools);
653
670
  const capabilityCatalog = buildAiCapabilityCatalog(registry).filter((entry) => !allowed || allowed.has(entry.name));
654
- const helpTools = input.listHelpRegistry ? createAiHelpTools(helpRegistry, input.locale) : [];
671
+ const helpTools = input.help ? createAiHelpTools(input.help, input.locale) : [];
655
672
  const resourceTool =
656
- input.execute && capabilityCatalog.length > 0
673
+ input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
657
674
  ? createAiResourceReaderTool({ apps: registry, catalog: capabilityCatalog, execute: input.execute })
658
675
  : null;
659
676
  const builtIns = [...input.staticTools, ...helpTools, ...(resourceTool ? [resourceTool] : [])].filter(
@@ -679,8 +696,10 @@ export const createAiToolResolver =
679
696
  ? createLoadedAiCapabilityTools({
680
697
  catalog: capabilityCatalog,
681
698
  loadedNames,
699
+ actor: input.actor,
682
700
  review: input.review,
683
701
  onReview: input.onReview,
702
+ authorizeBackground: input.authorizeBackground,
684
703
  execute: input.execute,
685
704
  })
686
705
  : []),
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { ToolContext } from "@k2b/nessi";
3
3
  import { type CapabilityDispatchDependencies, dispatchCapability } from "../api/capabilities";
4
+ import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
4
5
  import { type CapabilityActionManifest, type CapabilityActionReview, CapabilityActionReviewSchema } from "../contracts/capabilities";
5
6
  import type { RequestActor } from "../server";
6
7
  import type { AccessSubject } from "../server/services/access";
@@ -98,18 +99,24 @@ const dispatchAiCapability = async (input: AiCapabilityCall, review: boolean): P
98
99
  const headers = new Headers();
99
100
  if (input.locale) headers.set(LOCALE_HEADER, input.locale);
100
101
  const action = input.entry.kind === "action" ? (input.entry.operation as CapabilityActionManifest) : null;
101
- if (!review && action?.idempotency === "required" && input.context.callId) {
102
- const key = idempotencyKey(input.conversationId, input.context.callId);
103
- if (input.turnId) {
104
- await aiToolAudit.noteCapabilityDispatch({
105
- conversationId: input.conversationId,
106
- turnId: input.turnId,
107
- callId: input.context.callId,
108
- toolName: input.entry.name,
109
- idempotencyKey: key,
110
- });
111
- }
112
- headers.set("idempotency-key", key);
102
+ // One request id per dispatch links the tool call, the platform execution
103
+ // record, and the target app's own audit rows.
104
+ const requestId = crypto.randomUUID();
105
+ headers.set("x-request-id", requestId);
106
+ const key =
107
+ !review && action?.idempotency === "required" && input.context.callId
108
+ ? idempotencyKey(input.conversationId, input.context.callId)
109
+ : null;
110
+ if (key) headers.set("idempotency-key", key);
111
+ if (!review && input.turnId && input.context.callId) {
112
+ await aiToolAudit.noteCapabilityDispatch({
113
+ conversationId: input.conversationId,
114
+ turnId: input.turnId,
115
+ callId: input.context.callId,
116
+ toolName: input.entry.name,
117
+ requestId,
118
+ idempotencyKey: key,
119
+ });
113
120
  }
114
121
  const request = new Request("http://cloud.internal/api/ai/capability", {
115
122
  method: "POST",
@@ -120,6 +127,7 @@ const dispatchAiCapability = async (input: AiCapabilityCall, review: boolean): P
120
127
  await dispatch({
121
128
  request,
122
129
  kind: input.entry.kind === "query" ? "queries" : "actions",
130
+ origin: "assistant",
123
131
  review,
124
132
  appId: input.entry.appId,
125
133
  capabilityId: input.entry.operation.localId,
@@ -149,3 +157,30 @@ export const reviewAiCapability = async (input: AiCapabilityCall): Promise<Capab
149
157
  };
150
158
 
151
159
  export const executeAiCapability = (input: AiCapabilityCall): Promise<unknown> => dispatchAiCapability(input, false);
160
+
161
+ /**
162
+ * A rejected approval never reaches the dispatcher, so the assistant records
163
+ * the same execution row the dispatcher would have written for the attempt.
164
+ */
165
+ export const recordRejectedAiCapability = async (input: {
166
+ entry: Pick<AiCapabilityCatalogEntry, "appId" | "kind" | "name" | "operation">;
167
+ actor: CapabilityActor;
168
+ args: unknown;
169
+ }): Promise<void> => {
170
+ const at = new Date();
171
+ await recordCapabilityExecution({
172
+ requestId: crypto.randomUUID(),
173
+ origin: "assistant",
174
+ appId: input.entry.appId,
175
+ capability: input.entry.name,
176
+ kind: input.entry.kind,
177
+ destructive: "destructive" in input.entry.operation ? input.entry.operation.destructive : false,
178
+ actorKind: "user",
179
+ actorId: input.actor.user.id,
180
+ userId: input.actor.user.id,
181
+ status: "rejected",
182
+ inputMeta: capabilityValueMeta(input.args),
183
+ startedAt: at,
184
+ completedAt: at,
185
+ });
186
+ };
@@ -1,9 +1,11 @@
1
+ import { hasCapabilityTable } from "./capability-result";
1
2
  import { dates } from "@k2b/stdlib";
2
3
  import { mutation } from "@k2b/stdlib/solid";
3
- import { Button, ButtonLink, Chat, isStructuredDataValue, SplitButton, StructuredDataPreview, useLocale } from "@k2b/ui";
4
- import { createSignal, For, type JSX, Match, Show, Switch } from "solid-js";
4
+ import { Button, ButtonLink, Chat, MarkdownView, isStructuredDataValue, SplitButton, StructuredDataPreview, useLocale } from "@k2b/ui";
5
+ import { createMemo, createSignal, For, type JSX, Match, Show, Switch } from "solid-js";
5
6
  import type { CapabilityActionReview } from "../../contracts/capabilities";
6
7
  import { markdown } from "../../shared";
8
+ import { groupToolBlocks, isFailedTool, summarizeToolGroup } from "./tool-groups";
7
9
  import type { AiTurnBlock } from "../protocol";
8
10
  import { isRenderableTurnBlock } from "../protocol";
9
11
  import { hasSpecializedBuiltinToolView, SpecializedBuiltinToolBlock } from "./builtin-tools";
@@ -288,14 +290,23 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
288
290
  {(detail) => (
289
291
  <section class="min-w-0" aria-label={detail.label}>
290
292
  <h4 class="mb-1.5 font-semibold text-primary">{detail.label}</h4>
291
- <pre
292
- class="max-h-72 overflow-auto whitespace-pre-wrap break-words pr-2 font-sans text-xs leading-5 text-secondary"
293
- role="region"
294
- tabIndex={0}
295
- aria-label={`${detail.label} content`}
293
+ <Show
294
+ when={detail.format === "markdown"}
295
+ fallback={
296
+ <pre
297
+ class="max-h-72 overflow-auto whitespace-pre-wrap break-words pr-2 font-sans text-xs leading-5 text-secondary"
298
+ role="region"
299
+ tabIndex={0}
300
+ aria-label={`${detail.label} content`}
301
+ >
302
+ <ReviewDetailValue detail={detail} />
303
+ </pre>
304
+ }
296
305
  >
297
- <ReviewDetailValue detail={detail} />
298
- </pre>
306
+ <div class="max-h-72 overflow-auto text-xs text-secondary" role="region" tabIndex={0} aria-label={detail.label}>
307
+ <MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
308
+ </div>
309
+ </Show>
299
310
  </section>
300
311
  )}
301
312
  </For>
@@ -496,9 +507,6 @@ function CapabilityToolView(props: { block: ToolBlock }) {
496
507
  </Show>
497
508
  </Show>
498
509
  </Show>
499
- <Show when={props.block.status !== "running" && !props.block.isError}>
500
- <CapabilityTablePreview result={props.block.result} label={label()} />
501
- </Show>
502
510
  </>
503
511
  );
504
512
  }
@@ -618,85 +626,138 @@ function MemoryToolView(props: { block: ToolBlock }) {
618
626
  }
619
627
 
620
628
  function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
629
+ const actions = useAiChatActions();
621
630
  const status = () => props.block.status;
622
631
  const fetchError = () =>
623
632
  props.block.name === "fetch_file" && props.block.isError ? fetchFileErrorPresentation(props.block.result) : undefined;
624
633
  return (
625
- <Switch
626
- fallback={
627
- <ToolResultDisclosure
628
- blockId={props.block.id}
629
- name={displayToolName(props.block.name)}
630
- toolName={props.block.name}
631
- args={props.block.args}
632
- result={props.block.result}
633
- isError={Boolean(props.block.isError)}
634
- errorLabel={fetchError()?.label}
635
- errorDescription={fetchError()?.description}
636
- />
637
- }
638
- >
639
- <Match when={status() === "awaiting_approval"}>
640
- <ApprovalBlockView turnId={props.turnId} block={props.block} />
641
- </Match>
642
- <Match when={status() === "rejected"}>
643
- <RejectedToolView block={props.block} />
644
- </Match>
645
- <Match when={props.block.presentation?.kind === "capability"}>
646
- <CapabilityToolView block={props.block} />
647
- </Match>
648
- <Match when={props.block.name === "present" && !props.block.isError}>
649
- <PresentToolBlock block={props.block} />
650
- </Match>
651
- <Match when={props.block.name === "web_search" && !props.block.isError}>
652
- <WebSearchToolBlock block={props.block} />
653
- </Match>
654
- <Match when={props.block.name === "web_extract" && !props.block.isError}>
655
- <WebExtractToolBlock block={props.block} />
656
- </Match>
657
- <Match when={props.block.name === "fetch_file" && !props.block.isError}>
658
- <FetchFileToolBlock block={props.block} />
659
- </Match>
660
- <Match when={props.block.name === "memory"}>
661
- <MemoryToolView block={props.block} />
662
- </Match>
663
- <Match when={hasSpecializedBuiltinToolView(props.block.name, props.block.result) && status() === "completed" && !props.block.isError}>
664
- <SpecializedBuiltinToolBlock block={props.block} />
634
+ <>
635
+ <Switch
636
+ fallback={
637
+ <ToolResultDisclosure
638
+ blockId={props.block.id}
639
+ name={displayToolName(props.block.name)}
640
+ toolName={props.block.name}
641
+ args={props.block.args}
642
+ result={props.block.result}
643
+ isError={Boolean(props.block.isError)}
644
+ errorLabel={fetchError()?.label}
645
+ errorDescription={fetchError()?.description}
646
+ />
647
+ }
648
+ >
649
+ <Match when={status() === "awaiting_approval"}>
650
+ <ApprovalBlockView turnId={props.turnId} block={props.block} />
651
+ </Match>
652
+ <Match when={status() === "rejected"}>
653
+ <RejectedToolView block={props.block} />
654
+ </Match>
655
+ <Match when={props.block.presentation?.kind === "capability"}>
656
+ <CapabilityToolView block={props.block} />
657
+ </Match>
658
+ <Match when={actions.renderCodePresentation && props.block.name === "code_present" && status() === "completed" && !props.block.isError}>
659
+ {actions.renderCodePresentation?.(props.block.result)}
660
+ </Match>
661
+ <Match when={props.block.name === "present" && !props.block.isError}>
662
+ <PresentToolBlock block={props.block} />
663
+ </Match>
664
+ <Match when={props.block.name === "web_search" && !props.block.isError}>
665
+ <WebSearchToolBlock block={props.block} />
666
+ </Match>
667
+ <Match when={props.block.name === "web_extract" && !props.block.isError}>
668
+ <WebExtractToolBlock block={props.block} />
669
+ </Match>
670
+ <Match when={props.block.name === "fetch_file" && !props.block.isError}>
671
+ <FetchFileToolBlock block={props.block} />
672
+ </Match>
673
+ <Match when={props.block.name === "memory"}>
674
+ <MemoryToolView block={props.block} />
675
+ </Match>
676
+ <Match when={hasSpecializedBuiltinToolView(props.block.name, props.block.result) && status() === "completed" && !props.block.isError}>
677
+ <SpecializedBuiltinToolBlock block={props.block} />
678
+ </Match>
679
+ <Match when={isCardToolName(props.block.name) && !props.block.isError}>
680
+ <CloudCardBlock args={props.block.args} />
681
+ </Match>
682
+ <Match when={isSurveyToolName(props.block.name) && !props.block.isError}>
683
+ <SurveyToolView turnId={props.turnId} block={props.block} active={props.active} />
684
+ </Match>
685
+ <Match when={isTextEditorToolName(props.block.name)}>
686
+ <TextEditorToolView turnId={props.turnId} block={props.block} active={props.active} />
687
+ </Match>
688
+ <Match when={status() === "running" || status() === "awaiting_client"}>
689
+ <Chat.Activity label={displayToolName(props.block.name)} icon={aiToolIcon(props.block.name)} busy />
690
+ </Match>
691
+ </Switch>
692
+ <Show when={hasCapabilityTable(props.block)}>
693
+ <CapabilityTablePreview result={props.block.result} label={props.block.presentation?.title ?? displayToolName(props.block.name)} />
694
+ </Show>
695
+ </>
696
+ );
697
+ }
698
+
699
+ /** Render one unified turn block. Shared by persisted assistant groups and the live turn. */
700
+ export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; streaming?: boolean; active?: boolean }) {
701
+ // The id/kind remain stable while the immutable block value changes during a turn.
702
+ return (
703
+ <Switch>
704
+ <Match when={props.block.kind === "text" && props.block}>
705
+ {block => <AssistantMarkdownBlock html={markdown.renderSync(block().text)} />}
665
706
  </Match>
666
- <Match when={isCardToolName(props.block.name) && !props.block.isError}>
667
- <CloudCardBlock args={props.block.args} />
707
+ <Match when={props.block.kind === "thinking" && props.block}>
708
+ {block => <ThinkingBlockView text={block().text} streaming={props.streaming} />}
668
709
  </Match>
669
- <Match when={isSurveyToolName(props.block.name) && !props.block.isError}>
670
- <SurveyToolView turnId={props.turnId} block={props.block} active={props.active} />
710
+ <Match when={props.block.kind === "steer_applied"}>
711
+ <Chat.Activity label="Conversation steered" icon="ti ti-route" tone="ai" />
671
712
  </Match>
672
- <Match when={isTextEditorToolName(props.block.name)}>
673
- <TextEditorToolView turnId={props.turnId} block={props.block} active={props.active} />
713
+ <Match when={props.block.kind === "tool" && props.block}>
714
+ {block => <ToolBlockView turnId={props.turnId} block={block()} active={props.active} />}
674
715
  </Match>
675
- <Match when={status() === "running" || status() === "awaiting_client"}>
676
- <Chat.Activity label={displayToolName(props.block.name)} icon={aiToolIcon(props.block.name)} busy />
716
+ <Match when={props.block.kind === "compaction" && props.block}>
717
+ {block => <CompactionBlockView block={block()} />}
677
718
  </Match>
678
719
  </Switch>
679
720
  );
680
721
  }
681
722
 
682
- /** Render one unified turn block. Shared by persisted assistant groups and the live turn. */
683
- export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; streaming?: boolean; active?: boolean }) {
684
- // block.kind is immutable for a given block id, so this switch may run once.
685
- const block = props.block;
686
- switch (block.kind) {
687
- case "text":
688
- return <AssistantMarkdownBlock html={markdown.renderSync(block.text)} />;
689
- case "thinking":
690
- return <ThinkingBlockView text={block.text} streaming={props.streaming} />;
691
- case "steer_message":
692
- return null;
693
- case "steer_applied":
694
- return <Chat.Activity label="Conversation steered" icon="ti ti-route" tone="ai" />;
695
- case "tool":
696
- return <ToolBlockView turnId={props.turnId} block={block} active={props.active} />;
697
- case "compaction":
698
- return <CompactionBlockView block={block} />;
699
- }
723
+ function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
724
+ const locale = useLocale();
725
+ const de = () => locale().startsWith("de");
726
+ const args = () => isRecord(props.block.args) ? props.block.args : {};
727
+ const detail = () => [args().path, args().name, args().query].find((value) => typeof value === "string");
728
+ const waiting = () => props.block.status === "awaiting_client";
729
+ return <AiToolActivity blockId={props.block.id} label={props.block.presentation?.title ?? displayToolName(props.block.name)}
730
+ description={[props.block.status === "running" ? props.block.progress ?? "" : "", typeof detail() === "string" ? String(detail()) : "", waiting() ? (de() ? "Wartet" : "Waiting") : isFailedTool(props.block) ? (de() ? "Fehlgeschlagen" : "Failed") : ""].filter(Boolean).join(" · ")}
731
+ icon={waiting() ? "ti ti-clock" : aiToolIcon(props.block.name, props.block.presentation?.appIcon)}
732
+ tone={isFailedTool(props.block) ? "danger" : "neutral"} busy={props.busy && !waiting()} bodyInset={false} renderBody={() =>
733
+ <div class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950" tabIndex={0} role="region" aria-label={de() ? "Werkzeug-Eingabe und Ausgabe" : "Tool input and output"}>
734
+ <ToolDetail title={de() ? "Eingabe" : "Input"} toolName={props.block.name} value={props.block.args} />
735
+ <ToolDetail title={de() ? "Ausgabe" : "Output"} toolName={props.block.name} value={props.block.result} />
736
+ </div>}
737
+ />;
738
+ }
739
+
740
+ function ToolGroupView(props: { blocks: ToolBlock[]; active: boolean }) {
741
+ const locale = useLocale();
742
+ const last = () => props.blocks.at(-1)!;
743
+ const waiting = () => last().status === "awaiting_client";
744
+ const multiple = () => props.blocks.length > 1;
745
+ const detail = () => {
746
+ const args = last().args;
747
+ return isRecord(args) ? [args.path,args.name,args.query].find(value=>typeof value==="string") : undefined;
748
+ };
749
+ return <AiToolActivity blockId={`group:${props.blocks[0]!.id}`} bodyInset={false}
750
+ label={props.active || !multiple() ? (last().presentation?.title ?? displayToolName(last().name)) : summarizeToolGroup(props.blocks, locale())}
751
+ description={waiting() ? (locale().startsWith("de") ? "Wartet" : "Waiting") : last().status === "running" ? last().progress : !multiple() && typeof detail()==="string" ? String(detail()) : undefined}
752
+ icon={props.active || !multiple() ? (waiting() ? "ti ti-clock" : aiToolIcon(last().name)) : "ti ti-stack"}
753
+ tone={!multiple() && isFailedTool(last()) ? "danger" : "neutral"}
754
+ busy={props.active && !waiting()} renderBody={() => <Show when={multiple()} fallback={
755
+ <div class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950" tabIndex={0} role="region" aria-label={locale().startsWith("de") ? "Werkzeug-Eingabe und Ausgabe" : "Tool input and output"}>
756
+ <ToolDetail title={locale().startsWith("de") ? "Eingabe" : "Input"} toolName={last().name} value={last().args} />
757
+ <ToolDetail title={locale().startsWith("de") ? "Ausgabe" : "Output"} toolName={last().name} value={last().result} />
758
+ </div>
759
+ }><For each={props.blocks}>{(block) => <CompactToolRow block={block} />}</For></Show>}
760
+ />;
700
761
  }
701
762
 
702
763
  export function AiTurnBlockList(props: {
@@ -707,24 +768,25 @@ export function AiTurnBlockList(props: {
707
768
  active?: boolean;
708
769
  disclosureState?: AiToolDisclosureState;
709
770
  }) {
710
- const visible = () => props.blocks.filter(isRenderableTurnBlock);
771
+ const groups = createMemo(() => groupToolBlocks(props.blocks.filter(isRenderableTurnBlock)));
772
+ const keyedGroups = createMemo(() => new Map(groups().map(group => [group.kind === "tools" ? `tools:${group.blocks[0]!.id}` : `block:${group.block.id}`, group])));
773
+ const keys = createMemo(() => [...keyedGroups().keys()]);
711
774
  const disclosureState = props.disclosureState ?? createAiToolDisclosureState();
712
775
  return (
713
- <Show when={visible().length > 0}>
714
- <AiToolDisclosureProvider state={disclosureState}>
715
- <div class={`flex flex-col ${props.compact ? "gap-1" : "gap-2"}`}>
716
- <For each={visible()}>
717
- {(block, index) => (
718
- <AiTurnBlockView
719
- block={block}
720
- turnId={props.turnId}
721
- streaming={props.streaming && index() === visible().length - 1}
722
- active={props.active}
723
- />
724
- )}
725
- </For>
726
- </div>
727
- </AiToolDisclosureProvider>
728
- </Show>
776
+ <Show when={groups().length > 0}><AiToolDisclosureProvider state={disclosureState}>
777
+ <div class={`flex flex-col ${props.compact ? "gap-1" : "gap-2"}`}>
778
+ <For each={keys()}>{(key, index) => {
779
+ const group = () => keyedGroups().get(key)!;
780
+ return <Switch>
781
+ <Match when={(() => { const value = group(); return value.kind === "tools" ? value : undefined; })()}>
782
+ {(tools) => <ToolGroupView blocks={tools().blocks} active={Boolean(props.active && index() === keys().length - 1)} />}
783
+ </Match>
784
+ <Match when={(() => { const value = group(); return value.kind === "block" ? value : undefined; })()}>
785
+ {(item) => <AiTurnBlockView block={item().block} turnId={props.turnId} streaming={props.streaming && index() === keys().length - 1} active={props.active} />}
786
+ </Match>
787
+ </Switch>;
788
+ }}</For>
789
+ </div>
790
+ </AiToolDisclosureProvider></Show>
729
791
  );
730
792
  }
@@ -0,0 +1,15 @@
1
+ import { CapabilityTablePresentationSchema, capabilityDataAtPath } from "../../contracts/capabilities";
2
+ import type { AiTurnBlock } from "../protocol";
3
+
4
+ export const capabilityTable = (result: unknown) => {
5
+ if (!result || typeof result !== "object") return null;
6
+ const presentation = CapabilityTablePresentationSchema.safeParse(capabilityDataAtPath(result, ["presentation"]));
7
+ if (!presentation.success) return null;
8
+ const rows = capabilityDataAtPath(capabilityDataAtPath(result, ["data"]), presentation.data.rowsPath);
9
+ if (!Array.isArray(rows)) return null;
10
+ return { presentation: presentation.data, rows };
11
+ };
12
+
13
+ /** Result presentation is independent of optional app branding. */
14
+ export const hasCapabilityTable = (block: Extract<AiTurnBlock, { kind: "tool" }>): boolean =>
15
+ block.status === "completed" && !block.isError && capabilityTable(block.result) !== null;
@@ -1,17 +1,9 @@
1
+ import { capabilityTable } from "./capability-result";
1
2
  import { ButtonLink, DataTable, type DataTableColumn, useLocale } from "@k2b/ui";
2
3
  import { createMemo, For, Show } from "solid-js";
3
- import { CapabilitySemanticLinkSchema, CapabilityTablePresentationSchema, capabilityDataAtPath } from "../../contracts/capabilities";
4
+ import { CapabilitySemanticLinkSchema, capabilityDataAtPath } from "../../contracts/capabilities";
4
5
  import { aiChatMessages } from "./messages";
5
6
 
6
- export const capabilityTable = (result: unknown) => {
7
- if (!result || typeof result !== "object") return null;
8
- const presentation = CapabilityTablePresentationSchema.safeParse(capabilityDataAtPath(result, ["presentation"]));
9
- if (!presentation.success) return null;
10
- const rows = capabilityDataAtPath(capabilityDataAtPath(result, ["data"]), presentation.data.rowsPath);
11
- if (!Array.isArray(rows)) return null;
12
- return { presentation: presentation.data, rows };
13
- };
14
-
15
7
  export const capabilityCellText = (value: unknown): string => {
16
8
  if (value === undefined || value === null) return "—";
17
9
  if (typeof value === "string") return value;