@k2b/cloud 0.6.0-rc.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 (315) hide show
  1. package/README.md +2 -2
  2. package/package.json +30 -17
  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/desktop/solid.tsx +3 -3
  190. package/src/index.ts +0 -4
  191. package/src/server/index.ts +3 -0
  192. package/src/server/middleware/auth.ts +3 -2
  193. package/src/server/middleware/route-template.ts +9 -3
  194. package/src/server/middleware/runtime.ts +2 -1
  195. package/src/server/middleware/settings.ts +17 -11
  196. package/src/server/services/access-revision.ts +10 -0
  197. package/src/server/services/access.ts +1 -1
  198. package/src/server/services/index.ts +1 -0
  199. package/src/services/account-category-policy.ts +9 -0
  200. package/src/services/account-lifecycle/audit.ts +1 -1
  201. package/src/services/accounts/app.ts +12 -2
  202. package/src/services/accounts/email-write.ts +23 -0
  203. package/src/services/accounts/groups.ts +15 -4
  204. package/src/services/accounts/identities.ts +249 -0
  205. package/src/services/accounts/identity-reconciliation.ts +87 -0
  206. package/src/services/accounts/ipa-data.ts +2 -2
  207. package/src/services/accounts/local-groups.ts +2 -2
  208. package/src/services/accounts/posix.ts +45 -29
  209. package/src/services/accounts/users.ts +2 -2
  210. package/src/services/announcements/index.ts +69 -8
  211. package/src/services/audit/index.ts +1 -1
  212. package/src/services/cache-fill.ts +35 -0
  213. package/src/services/help/index.ts +109 -0
  214. package/src/services/help/maintenance.ts +25 -0
  215. package/src/services/help/store.ts +180 -0
  216. package/src/services/help/types.ts +46 -0
  217. package/src/services/identity/invocation-actor.ts +2 -2
  218. package/src/services/identity/key-config.ts +2 -1
  219. package/src/services/identity/key-ring.ts +1 -1
  220. package/src/services/index.ts +15 -0
  221. package/src/services/ipa/users.ts +268 -221
  222. package/src/services/legal-consent.ts +1 -1
  223. package/src/services/logging/index.ts +15 -13
  224. package/src/services/logging/trace.ts +2 -1
  225. package/src/services/logging/web-vitals.ts +81 -0
  226. package/src/services/mandates/index.ts +18 -4
  227. package/src/services/mandates/policy.ts +65 -2
  228. package/src/services/notifications/batches.ts +124 -57
  229. package/src/services/notifications/browser.ts +12 -0
  230. package/src/services/notifications/index.ts +0 -2
  231. package/src/services/notifications/platform.ts +0 -4
  232. package/src/services/oauth-tokens.ts +3 -3
  233. package/src/services/pdf/gotenberg.ts +107 -84
  234. package/src/services/pdf/index.ts +3 -0
  235. package/src/services/providers/local/users.ts +25 -16
  236. package/src/services/public-http.ts +101 -0
  237. package/src/services/rail-shortcuts.ts +95 -0
  238. package/src/services/rail-snapshot.ts +53 -0
  239. package/src/services/request-cache-redis.ts +28 -0
  240. package/src/services/session/index.ts +15 -2
  241. package/src/services/session/user.ts +62 -37
  242. package/src/services/settings/core-settings.ts +21 -28
  243. package/src/services/settings/defaults.ts +5 -0
  244. package/src/services/settings/store.ts +58 -84
  245. package/src/shared/ai-costs.ts +42 -0
  246. package/src/shared/ai-platform-prompt.ts +1 -1
  247. package/src/shared/ai-quotas.ts +136 -0
  248. package/src/shared/ai-usage.ts +6 -16
  249. package/src/shared/app-presentation.ts +21 -15
  250. package/src/shared/capability-messages.ts +14 -0
  251. package/src/shared/help.ts +6 -42
  252. package/src/shared/index.ts +6 -0
  253. package/src/ssr/AdminLayout.tsx +7 -5
  254. package/src/ssr/AdminSidebar.tsx +29 -15
  255. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  256. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  257. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  258. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  259. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  260. package/src/ssr/Layout.tsx +17 -6
  261. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  262. package/src/ssr/LayoutFooter.tsx +1 -1
  263. package/src/ssr/LayoutHeader.tsx +46 -14
  264. package/src/ssr/LayoutHelp.tsx +23 -22
  265. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  266. package/src/ssr/LayoutRail.tsx +17 -9
  267. package/src/ssr/MobileNavigation.tsx +71 -0
  268. package/src/ssr/MobileProfileActions.tsx +49 -0
  269. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  270. package/src/ssr/RailApps.island.tsx +2 -2
  271. package/src/ssr/RailEditor.tsx +17 -2
  272. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  273. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  274. package/src/ssr/admin-navigation.ts +7 -1
  275. package/src/ssr/help.ts +29 -0
  276. package/src/ssr/islands/SearchBar.tsx +67 -0
  277. package/src/ssr/islands/index.ts +3 -1
  278. package/src/ssr/layout-context.ts +2 -2
  279. package/src/ssr/layout-help-search.ts +15 -0
  280. package/src/ssr/mobile-menu-history.ts +70 -0
  281. package/src/ssr/platform-messages.ts +16 -8
  282. package/src/ssr/profile-actions.ts +42 -0
  283. package/src/ssr/rail-context.ts +8 -4
  284. package/src/ssr/rail-messages.ts +2 -0
  285. package/src/ssr/rail-navigation.ts +3 -3
  286. package/src/ssr/workspace-navigation.ts +75 -0
  287. package/src/styles/effects.css +6 -6
  288. package/src/styles/global.css +54 -0
  289. package/src/styles/input.css +43 -41
  290. package/src/styles/resource-search.css +505 -0
  291. package/src/styles/utilities-feedback.css +13 -0
  292. package/src/styles/utilities-layout.css +1 -5
  293. package/src/styles/utilities-navigation.css +3 -39
  294. package/src/types/ambient.d.ts +5 -0
  295. package/src/workflows/ai/runtime.ts +5 -1
  296. package/src/workflows/ai/store.ts +2 -0
  297. package/src/workflows/ai-actions.ts +16 -13
  298. package/src/workflows/definition.ts +10 -5
  299. package/src/workflows/store/actions.ts +44 -20
  300. package/src/workflows/store/index.ts +2 -0
  301. package/src/workflows/store/runs.ts +36 -0
  302. package/src/workflows/store/worker-pool.ts +55 -0
  303. package/src/workflows/store/worker-runtime.ts +90 -0
  304. package/scripts/app-favicon.test.ts +0 -49
  305. package/src/ai/chat/blocks.render.test.tsx +0 -1108
  306. package/src/ai/kit-skill.ts +0 -22
  307. package/src/ai/usage-migrate.ts +0 -81
  308. package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
  309. package/src/browser/notification-target.ts +0 -27
  310. package/src/contracts/notification-live.ts +0 -66
  311. package/src/services/notifications/live.ts +0 -50
  312. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  313. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
  314. package/src/ssr/Layout.render.test.tsx +0 -148
  315. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -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;
@@ -1,7 +1,7 @@
1
- import type { ChatAttachment, ChatModelOption, ChatSubmitInput } from "@k2b/ui";
1
+ import type { ChatAttachment, ChatMention, ChatModelOption, ChatSubmitInput } from "@k2b/ui";
2
2
  import { AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "../limits";
3
3
  import type { AiResourceMarker } from "../resource-markers";
4
- import type { AiPublicModelProfile, AiUserContentPart } from "../types";
4
+ import type { AiDraftContentPart, AiPublicModelProfile, AiUserContentPart } from "../types";
5
5
  import {
6
6
  type AiComposerAttachment,
7
7
  FILE_INPUT_ACCEPT,
@@ -21,6 +21,7 @@ const AI_COMPOSER_TEXT_MAX_BYTES = AI_COMPOSER_TEXT_MAX_CHARS * 4;
21
21
  const AI_PASTED_TEXT_FILE_NAME = /^pasted-(?:text|[a-f0-9]{8})(?:-\d+)?\.txt$/i;
22
22
 
23
23
  export type AiComposerSendInput = {
24
+ draftContent?: AiDraftContentPart[];
24
25
  message?: string;
25
26
  content?: AiUserContentPart[];
26
27
  files?: File[];
@@ -96,7 +97,7 @@ const isAiComposerAttachment = (value: unknown): value is AiComposerAttachment =
96
97
  if (!value || typeof value !== "object") return false;
97
98
  const candidate = value as Partial<AiComposerAttachment>;
98
99
  return (
99
- ["image", "file", "stored-file", "resource"].includes(candidate.kind ?? "") &&
100
+ ["image", "file", "stored-file", "resource", "project-file"].includes(candidate.kind ?? "") &&
100
101
  typeof candidate.id === "string" &&
101
102
  typeof candidate.name === "string"
102
103
  );
@@ -107,6 +108,24 @@ export const aiComposerAttachmentRecords = (attachments: readonly ChatAttachment
107
108
 
108
109
  export const aiComposerSendInput = (input: ChatSubmitInput): AiComposerSendInput => {
109
110
  const attachments = aiComposerAttachmentRecords(input.attachments);
111
+ if (input.mentions?.length || attachments.some(attachment => attachment.kind === "project-file")) {
112
+ const draftContent: AiDraftContentPart[] = [];
113
+ let offset = 0;
114
+ for (const mention of [...(input.mentions ?? [])].sort((a,b) => a.start - b.start)) {
115
+ if (mention.start < offset || mention.end > input.text.length || mention.start >= mention.end) continue;
116
+ const attachment = aiComposerAttachmentRecords([mention.attachment])[0];
117
+ if (!attachment || attachment.kind === "file" || attachment.kind === "image") continue;
118
+ if (mention.start > offset) draftContent.push({ type: "text", text: input.text.slice(offset, mention.start) });
119
+ draftContent.push(composerAttachmentPart(attachment, true));
120
+ offset = mention.end;
121
+ }
122
+ if (offset < input.text.length) draftContent.push({ type: "text", text: input.text.slice(offset) });
123
+ for (const attachment of attachments) {
124
+ if (attachment.kind !== "file" && attachment.kind !== "image") draftContent.push(composerAttachmentPart(attachment));
125
+ }
126
+ const files = attachments.flatMap(attachment => attachment.kind === "file" || attachment.kind === "image" ? [attachment.file] : []);
127
+ return { draftContent, files: files.length ? files : undefined };
128
+ }
110
129
  const files = attachments.filter(
111
130
  (attachment): attachment is PendingAiImage | PendingAiVfsFile => attachment.kind === "image" || attachment.kind === "file",
112
131
  );
@@ -167,3 +186,29 @@ export const readAiComposerFiles = async (
167
186
  discarded: Math.max(0, files.length - candidates.length),
168
187
  };
169
188
  };
189
+
190
+ const composerAttachmentPart = (attachment: Exclude<AiComposerAttachment, {kind: "image" | "file"}>, inline = false): AiDraftContentPart => {
191
+ if (attachment.kind === "resource") return { type: "resource", ref: attachment.ref, title: attachment.name, icon: attachment.icon, href: attachment.href, inline };
192
+ if (attachment.kind === "project-file") return { type: "project-file", path: attachment.path, inline };
193
+ return { type: "file", path: attachment.path, version: attachment.version, mediaType: attachment.mediaType, size: attachment.size, inline };
194
+ };
195
+
196
+ /** Hydrate the same ordered content used by the server draft and queued messages. */
197
+ export function aiComposerDraft(content: readonly AiDraftContentPart[]): { text: string; mentions: ChatMention[]; attachments: AiComposerAttachment[] } {
198
+ let text = "";
199
+ const mentions: ChatMention[] = [];
200
+ const attachments: AiComposerAttachment[] = [];
201
+ for (const part of content) {
202
+ if (part.type === "text") { text += part.text; continue; }
203
+ const attachment: AiComposerAttachment = part.type === "resource"
204
+ ? { kind: "resource", id: `resource:${part.ref.type}:${part.ref.id}`, ref: part.ref, name: part.title ?? part.ref.id, icon: part.icon ?? "ti ti-link", href: part.href }
205
+ : part.type === "project-file"
206
+ ? { kind: "project-file", id: part.path, path: part.path, name: part.path, icon: "ti ti-file" }
207
+ : { kind: "stored-file", id: `file:${part.path}:${part.version}`, path: part.path, name: part.path.split("/").at(-1) || part.path, version: part.version, mediaType: part.mediaType, size: part.size, icon: "ti ti-file" };
208
+ if (part.inline) {
209
+ mentions.push({ start: text.length, end: text.length + attachment.name.length, attachment: aiChatAttachments([attachment])[0]! });
210
+ text += attachment.name;
211
+ } else attachments.push(attachment);
212
+ }
213
+ return { text, mentions, attachments };
214
+ }
@@ -28,6 +28,8 @@ type RetryMessageHandler = (entry: AiStoredMessage, input?: AiRetryMessageInput)
28
28
  type RetrySteerHandler = (block: Extract<AiTurnBlock, { kind: "steer_message" }>) => void | Promise<void>;
29
29
 
30
30
  export type AiChatActions = {
31
+ /** Host-owned visualizations, persisted independently of the execution session. */
32
+ renderCodePresentation?: (result: unknown) => JSX.Element;
31
33
  /** Prevents turn-continuation actions while the current turn is stopping. */
32
34
  actionDisabled?: () => boolean;
33
35
  onApproval?: ApprovalHandler;
@@ -36,8 +38,12 @@ export type AiChatActions = {
36
38
  onRetryMessage?: RetryMessageHandler;
37
39
  onRetrySteer?: RetrySteerHandler;
38
40
  onMessageFeedback?: (entry: AiStoredMessage, feedback: Omit<AiMessageFeedback, "updatedAt"> | null) => void | Promise<void>;
41
+ /** Open the originating background run for a delivered scheduled result. */
42
+ onOpenScheduledTaskRun?: (taskId: string, occurrenceId: string) => void;
39
43
  /** Open a conversation VFS file in the host application's artifact surface. */
40
44
  onOpenFile?: (path: string) => void;
45
+ /** Resolve a Markdown link only when it identifies a known conversation file. */
46
+ resolveFileLink?: (href: string) => { path: string; href: string } | null;
41
47
  /** Download URL for a conversation VFS file (present blocks, attachment chips). */
42
48
  fileUrl?: (path: string) => string | null;
43
49
  };
@@ -53,8 +59,8 @@ export function AiChatActionsProvider(props: { actions?: AiChatActions; children
53
59
  return <AiChatActionContext.Provider value={props.actions ?? {}}>{props.children}</AiChatActionContext.Provider>;
54
60
  }
55
61
 
56
- const usageValue = (usage: Usage | null | undefined, key: "input" | "output" | "total" | "creditsUsed") => {
57
- const value = (usage as Partial<Record<"input" | "output" | "total" | "creditsUsed", unknown>> | null | undefined)?.[key];
62
+ const usageValue = (usage: Usage | null | undefined, key: "input" | "output" | "total") => {
63
+ const value = (usage as Partial<Record<"input" | "output" | "total", unknown>> | null | undefined)?.[key];
58
64
  return typeof value === "number" && Number.isFinite(value) ? value : null;
59
65
  };
60
66
 
@@ -70,7 +76,6 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
70
76
  const toolCalls = blocks.filter((block) => block.type === "tool_call");
71
77
  const aggregate = entry.loopAggregate;
72
78
  const usage = aggregate?.usage ?? entry.usage;
73
- const credits = usageValue(usage, "creditsUsed");
74
79
  const timing = aggregate?.timing;
75
80
 
76
81
  const meta = [
@@ -100,7 +105,6 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
100
105
  input: usageValue(usage, "input"),
101
106
  output: usageValue(usage, "output"),
102
107
  total: usageValue(usage, "total"),
103
- credits,
104
108
  },
105
109
  turns: aggregate?.assistantMessageCount ?? 1,
106
110
  toolCallCount: aggregate?.toolCallCount ?? toolCalls.length,
@@ -184,9 +188,6 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
184
188
  accent={{ tone: "red", icon: "ti ti-alert-triangle" }}
185
189
  />
186
190
  </Show>
187
- <Show when={info.usage.credits !== null && info.usage.credits > 0}>
188
- <StatCell label="Credits" value={info.usage.credits?.toLocaleString(locale, { maximumFractionDigits: 6 }) ?? "–"} />
189
- </Show>
190
191
  </StatGrid>
191
192
 
192
193
  <Show when={info.tools.length > 0}>
@@ -1,3 +1,4 @@
1
+ import { AI_AUDIO_EXTENSIONS } from "../audio-format";
1
2
  import type { Message, Usage } from "@k2b/nessi";
2
3
  import { fileIcons } from "@k2b/stdlib";
3
4
  import { formatBytes as sharedFormatBytes } from "../../shared/format";
@@ -20,6 +21,7 @@ export type AiForkMessageInput = {
20
21
  };
21
22
 
22
23
  export type AiComposerAttachment =
24
+ | { kind: "project-file"; id: string; name: string; path: string; icon: string }
23
25
  | {
24
26
  kind: "image";
25
27
  id: string;
@@ -111,6 +113,8 @@ export const TEXT_ATTACHMENT_MEDIA_TYPES = new Set([
111
113
  ]);
112
114
  export const FILE_INPUT_ACCEPT = [
113
115
  ...AI_IMAGE_MEDIA_TYPES,
116
+ "audio/*",
117
+ ...AI_AUDIO_EXTENSIONS.map((extension) => `.${extension}`),
114
118
  "text/*",
115
119
  ...Array.from(TEXT_ATTACHMENT_MEDIA_TYPES),
116
120
  ...Array.from(new Set([...TEXT_ATTACHMENT_EXTENSIONS, ...DOCUMENT_ATTACHMENT_EXTENSIONS]), (extension) => `.${extension}`),
@@ -228,14 +232,20 @@ export type AiLatestUsageSnapshot = {
228
232
  export const latestUsageSnapshot = (messages: AiStoredMessage[]): AiLatestUsageSnapshot | null => {
229
233
  for (let i = messages.length - 1; i >= 0; i--) {
230
234
  const entry = messages[i];
235
+ if (entry?.message.role !== "assistant") continue;
231
236
  const request = entry?.loopAggregate?.turns.findLast((turn) => Boolean(turn.usage))?.usage ?? entry?.usage;
232
237
  if (request) {
233
238
  return {
234
239
  request,
235
- loop: entry?.loopAggregate?.usage ?? entry?.usage ?? request,
240
+ loop: entry?.loopAggregate?.usage ?? (entry?.loopId ? messages.filter(message => message.loopId === entry.loopId && message.message.role === "assistant").reduce<Usage>((sum, message) => ({
241
+ input: sum.input + (message.usage?.input ?? 0), output: sum.output + (message.usage?.output ?? 0), total: sum.total + (message.usage?.total ?? 0),
242
+ ...((sum.cacheRead !== undefined || message.usage?.cacheRead !== undefined) ? { cacheRead: (sum.cacheRead ?? 0) + (message.usage?.cacheRead ?? 0) } : {}),
243
+ }), { input: 0, output: 0, total: 0 }) : request),
236
244
  modelProfileId: entry?.modelProfileId ?? null,
237
245
  };
238
246
  }
247
+ // A provider without usage must not inherit a previous request's numbers.
248
+ return null;
239
249
  }
240
250
  return null;
241
251
  };
@@ -294,6 +304,7 @@ const BUILT_IN_TOOL_ICONS = new Map<string, string>([
294
304
  ["search_tools", "ti ti-ai-gateway"],
295
305
  ["load_tools", "ti ti-ai-gateway"],
296
306
  ["load_skill", "ti ti-sparkles"],
307
+ ["todo_write", "ti ti-list-check"],
297
308
  ["list_apps", "ti ti-apps"],
298
309
  ["read_cloud_resource", "ti ti-ai-gateway"],
299
310
  ]);
@@ -4,6 +4,7 @@ const messages = i18n.define({
4
4
  baseLocale: "en",
5
5
  messages: {
6
6
  en: {
7
+ backgroundRun: "Background run",
7
8
  tableLinks: "Links",
8
9
  tableOpen: "Open",
9
10
  tableEmpty: "No matching rows.",
@@ -20,6 +21,7 @@ const messages = i18n.define({
20
21
  byteRange: ({ start, end }: { start: string; end: string }) => `Bytes ${start}–${end}`,
21
22
  },
22
23
  de: {
24
+ backgroundRun: "Hintergrundlauf",
23
25
  tableLinks: "Links",
24
26
  tableOpen: "Öffnen",
25
27
  tableEmpty: "Keine passenden Zeilen.",