@k2b/cloud 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -2,6 +2,7 @@ import type { Message, Usage } from "@k2b/nessi";
2
2
  import { fileIcons } from "@k2b/stdlib";
3
3
  import { formatBytes as sharedFormatBytes } from "../../shared/format";
4
4
  import { type AiAttachmentRef, parseAiAttachmentMarkers } from "../attachments";
5
+ import { AI_AUDIO_EXTENSIONS } from "../audio-format";
5
6
  import { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS } from "../limits";
6
7
  import { type AiResourceMarker, parseAiResourceMarker } from "../resource-markers";
7
8
  import { assistantVisibleTextFromMessage } from "../timeline";
@@ -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,33 @@ 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:
241
+ entry?.loopAggregate?.usage ??
242
+ (entry?.loopId
243
+ ? messages
244
+ .filter((message) => message.loopId === entry.loopId && message.message.role === "assistant")
245
+ .reduce<Usage>(
246
+ (sum, message) => ({
247
+ input: sum.input + (message.usage?.input ?? 0),
248
+ output: sum.output + (message.usage?.output ?? 0),
249
+ total: sum.total + (message.usage?.total ?? 0),
250
+ ...(sum.cacheRead !== undefined || message.usage?.cacheRead !== undefined
251
+ ? { cacheRead: (sum.cacheRead ?? 0) + (message.usage?.cacheRead ?? 0) }
252
+ : {}),
253
+ }),
254
+ { input: 0, output: 0, total: 0 },
255
+ )
256
+ : request),
236
257
  modelProfileId: entry?.modelProfileId ?? null,
237
258
  };
238
259
  }
260
+ // A provider without usage must not inherit a previous request's numbers.
261
+ return null;
239
262
  }
240
263
  return null;
241
264
  };
@@ -294,6 +317,7 @@ const BUILT_IN_TOOL_ICONS = new Map<string, string>([
294
317
  ["search_tools", "ti ti-ai-gateway"],
295
318
  ["load_tools", "ti ti-ai-gateway"],
296
319
  ["load_skill", "ti ti-sparkles"],
320
+ ["todo_write", "ti ti-list-check"],
297
321
  ["list_apps", "ti ti-apps"],
298
322
  ["read_cloud_resource", "ti ti-ai-gateway"],
299
323
  ]);
@@ -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.",
@@ -1,15 +1,14 @@
1
- import { Chat, type ChatTimelineItem, useLocale } from "@k2b/ui";
1
+ import { Button, Chat, type ChatTimelineItem, useLocale } from "@k2b/ui";
2
2
  import { type Accessor, createEffect, createMemo, createSignal, type JSX, onCleanup, Show } from "solid-js";
3
3
  import type { AiActiveTurn } from "../client/projection";
4
- import { type AiActiveTurnSegment, isRenderableTurnBlock, splitActiveTurnBlocks } from "../protocol";
4
+ import { type AiActiveTurnSegment, splitActiveTurnBlocks } from "../protocol";
5
5
  import { type AiAssistantTimelineItem, buildAiMessageTimeline, copyTextFromAssistantEntries } from "../timeline";
6
6
  import type { AiConversationTimelineEntry, AiStoredMessage } from "../types";
7
7
  import { AiTurnBlockList } from "./blocks";
8
8
  import { type AiChatActions, AiChatActionsProvider, createAssistantMessageActions, useAiChatActions } from "./message-actions";
9
- import { formatWorkedDuration, isCardToolName, isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
10
- import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
9
+ import { formatWorkedDuration, isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
11
10
  import { aiChatMessages } from "./messages";
12
- import { CloudSurveyResultBlock } from "./visual-tools";
11
+ import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
13
12
  import { TurnNavigator } from "./turn-navigator";
14
13
  import { activeTimelineSeq } from "./turn-navigator-utils";
15
14
  import {
@@ -19,6 +18,7 @@ import {
19
18
  createAiSteerMessageActions,
20
19
  createAiUserMessageActions,
21
20
  } from "./user-message";
21
+ import { CloudSurveyResultBlock } from "./visual-tools";
22
22
 
23
23
  export type AiChatTimelineSession = {
24
24
  messages: readonly AiStoredMessage[];
@@ -63,51 +63,20 @@ const surveyItem = (block: SurveyResultBlock, turnId: string): ChatTimelineItem
63
63
  content: <CloudSurveyResultBlock args={block.args} result={block.result} />,
64
64
  });
65
65
 
66
- const isDirectCompletedResult = (block: AssistantBlock): boolean =>
67
- block.kind === "tool" && block.status === "completed" && !block.isError && (isCardToolName(block.name) || block.name === "present");
68
-
69
- const isFailedBlock = (block: AssistantBlock): boolean =>
70
- (block.kind === "tool" && (block.isError || block.status === "failed" || block.status === "rejected")) ||
71
- (block.kind === "compaction" && block.status === "failed");
72
-
73
- export const partitionCompletedAssistantBlocks = (blocks: AssistantBlock[]) => {
74
- const renderable = blocks.filter(isRenderableTurnBlock);
75
- const finalTextIndex = renderable.findLastIndex((block) => block.kind === "text");
76
- return {
77
- worked: renderable.filter((block, index) => index !== finalTextIndex && !isDirectCompletedResult(block)),
78
- visible: renderable.filter((block, index) => index === finalTextIndex || isDirectCompletedResult(block)),
79
- };
80
- };
81
-
82
66
  export function AiAssistantContent(props: {
83
67
  item: AiAssistantTimelineItem;
84
68
  disclosureState?: AiToolDisclosureState;
85
69
  segmentId?: string;
86
70
  }): JSX.Element {
87
71
  const locale = useLocale();
88
- const blocks = createMemo(() => partitionCompletedAssistantBlocks(props.item.blocks));
89
72
  const turnId = () => props.item.loopId ?? props.item.id;
90
- const workedDisclosureId = () => `worked:${props.segmentId ?? turnId()}`;
91
- const workedOpen = () => props.disclosureState?.get(workedDisclosureId());
92
- const setWorkedOpen = (open: boolean) => props.disclosureState?.set(workedDisclosureId(), open);
93
- const workedFailed = () => blocks().worked.some(isFailedBlock);
94
-
95
73
  return (
96
74
  <div class="flex flex-col gap-2">
97
- <Show when={blocks().worked.length > 0}>
98
- <Chat.Activity
99
- icon="ti ti-route"
100
- label={props.segmentId ? aiChatMessages(locale()).workSteps : `Worked for ${formatWorkedDuration(props.item.workedMs)}`}
101
- tone={workedFailed() ? "danger" : undefined}
102
- bodyInset={false}
103
- defaultOpen={workedFailed()}
104
- open={workedOpen()}
105
- onOpenChange={setWorkedOpen}
106
- >
107
- <AiTurnBlockList blocks={blocks().worked} turnId={turnId()} compact disclosureState={props.disclosureState} />
108
- </Chat.Activity>
109
- </Show>
110
- <AiTurnBlockList blocks={blocks().visible} turnId={turnId()} disclosureState={props.disclosureState} />
75
+ <Chat.Activity
76
+ icon="ti ti-route"
77
+ label={`${locale().startsWith("de") ? "Gearbeitet für" : "Worked for"} ${formatWorkedDuration(props.item.workedMs)}`}
78
+ />
79
+ <AiTurnBlockList blocks={props.item.blocks} turnId={turnId()} disclosureState={props.disclosureState} />
111
80
  </div>
112
81
  );
113
82
  }
@@ -206,11 +175,26 @@ const storedItems = (
206
175
  createdAt: actionEntry?.createdAt ?? item.entries.at(-1)?.createdAt,
207
176
  class: segment.blocks.some(isWideBlock) ? "ai-chat-message-wide" : undefined,
208
177
  content: (
209
- <AiAssistantContent
210
- item={{ ...item, blocks: segment.blocks }}
211
- segmentId={segments.length > 1 ? `${item.id}:${index}` : undefined}
212
- disclosureState={disclosureState}
213
- />
178
+ <>
179
+ <Show when={index === 0 && item.entries.find((entry) => entry.meta?.scheduledTask)?.meta?.scheduledTask}>
180
+ {(task) => (
181
+ <Button
182
+ variant="ghost"
183
+ size="xs"
184
+ class="mb-2"
185
+ onClick={() => actions.onOpenScheduledTaskRun?.(task().taskId, task().occurrenceId)}
186
+ disabled={!actions.onOpenScheduledTaskRun}
187
+ >
188
+ <i class="ti ti-calendar-time" aria-hidden="true" /> {aiChatMessages(locale).backgroundRun}
189
+ </Button>
190
+ )}
191
+ </Show>
192
+ <AiAssistantContent
193
+ item={{ ...item, blocks: segment.blocks }}
194
+ segmentId={segments.length > 1 ? `${item.id}:${index}` : undefined}
195
+ disclosureState={disclosureState}
196
+ />
197
+ </>
214
198
  ),
215
199
  actions:
216
200
  actionEntry && index === lastAssistant
@@ -268,7 +252,7 @@ const activeItems = (turn: AiActiveTurn | null, actions: AiChatActions, disclosu
268
252
  blocks={blocks}
269
253
  turnId={turn.turnId}
270
254
  streaming={turn.status === "running" && index === segments.length - 1}
271
- active
255
+ active={index === segments.length - 1}
272
256
  disclosureState={disclosureState}
273
257
  />
274
258
  ),
@@ -1,3 +1,45 @@
1
+ import { createEffect, onCleanup, onMount } from "solid-js";
2
+ import { useAiChatActions } from "./message-actions";
3
+
1
4
  export function AssistantMarkdownBlock(props: { html: string }) {
2
- return <div class="assistant-markdown-block" innerHTML={props.html} />;
5
+ const actions = useAiChatActions();
6
+ let element!: HTMLDivElement;
7
+ const originals = new WeakMap<HTMLAnchorElement, { href: string; target: string | null }>();
8
+ const files = new WeakMap<HTMLAnchorElement, string>();
9
+ createEffect(() => {
10
+ props.html;
11
+ for (const link of Array.from(element.querySelectorAll<HTMLAnchorElement>("a[href]"))) {
12
+ const original = originals.get(link) ?? { href: link.getAttribute("href")!, target: link.getAttribute("target") };
13
+ originals.set(link, original);
14
+ const file = actions.resolveFileLink?.(original.href);
15
+ link.setAttribute("href", file?.href ?? original.href);
16
+ const target = file ? null : original.target;
17
+ if (target) link.setAttribute("target", target);
18
+ else link.removeAttribute("target");
19
+ if (file) files.set(link, file.path);
20
+ else files.delete(link);
21
+ }
22
+ });
23
+ onMount(() => {
24
+ const handleClick = (event: MouseEvent) => {
25
+ if (
26
+ event.defaultPrevented ||
27
+ event.button !== 0 ||
28
+ event.metaKey ||
29
+ event.ctrlKey ||
30
+ event.shiftKey ||
31
+ event.altKey ||
32
+ !actions.onOpenFile
33
+ )
34
+ return;
35
+ const link = event.target instanceof Element ? event.target.closest("a") : null;
36
+ const path = link ? files.get(link) : undefined;
37
+ if (!path) return;
38
+ event.preventDefault();
39
+ actions.onOpenFile(path);
40
+ };
41
+ element.addEventListener("click", handleClick);
42
+ onCleanup(() => element.removeEventListener("click", handleClick));
43
+ });
44
+ return <div ref={element} class="assistant-markdown-block" innerHTML={props.html} />;
3
45
  }
@@ -8,9 +8,28 @@ export type AiToolDisclosureState = {
8
8
 
9
9
  export const createAiToolDisclosureState = (): AiToolDisclosureState => {
10
10
  const openByBlockId = new Map<string, boolean>();
11
+ const storageKey = (blockId: string) => `cloud.ai.tool-disclosure:${blockId}`;
11
12
  return {
12
- get: (blockId) => openByBlockId.get(blockId),
13
- set: (blockId, open) => openByBlockId.set(blockId, open),
13
+ get: (blockId) => {
14
+ if (!openByBlockId.has(blockId)) {
15
+ try {
16
+ const saved = window.sessionStorage.getItem(storageKey(blockId));
17
+ if (saved === "open") openByBlockId.set(blockId, true);
18
+ } catch {
19
+ /* SSR and disabled storage keep the in-memory state. */
20
+ }
21
+ }
22
+ return openByBlockId.get(blockId);
23
+ },
24
+ set: (blockId, open) => {
25
+ openByBlockId.set(blockId, open);
26
+ try {
27
+ if (open) window.sessionStorage.setItem(storageKey(blockId), "open");
28
+ else window.sessionStorage.removeItem(storageKey(blockId));
29
+ } catch {
30
+ /* Storage limits must not prevent expanding a tool. */
31
+ }
32
+ },
14
33
  };
15
34
  };
16
35
 
@@ -0,0 +1,58 @@
1
+ import type { AiTurnBlock } from "../protocol";
2
+ import { hasCapabilityTable } from "./capability-result";
3
+ import { isCardToolName, isSurveyToolName, isTextEditorToolName } from "./message-utils";
4
+
5
+ type Tool = Extract<AiTurnBlock, { kind: "tool" }>;
6
+ type Group = { kind: "tools"; blocks: Tool[] } | { kind: "block"; block: AiTurnBlock };
7
+
8
+ export function isFailedTool(tool: Tool): boolean {
9
+ if (tool.isError || tool.status === "failed" || tool.status === "rejected") return true;
10
+ const result = tool.result;
11
+ return (
12
+ ["code_run", "code_inspect", "code_interact"].includes(tool.name) &&
13
+ result !== null &&
14
+ typeof result === "object" &&
15
+ "status" in result &&
16
+ result.status === "error"
17
+ );
18
+ }
19
+
20
+ /** Visible content and interactive decisions are boundaries; model request boundaries are not. */
21
+ export function groupToolBlocks(blocks: readonly AiTurnBlock[]): Group[] {
22
+ const groups: Group[] = [];
23
+ for (const block of blocks) {
24
+ const ordinary =
25
+ block.kind === "tool" &&
26
+ !block.approval &&
27
+ block.status !== "awaiting_approval" &&
28
+ !["present", "code_present", "view_image", "code_secret"].includes(block.name) &&
29
+ !isCardToolName(block.name) &&
30
+ !isSurveyToolName(block.name) &&
31
+ !isTextEditorToolName(block.name) &&
32
+ !hasCapabilityTable(block);
33
+ if (ordinary) {
34
+ const last = groups.at(-1);
35
+ if (last?.kind === "tools") last.blocks.push(block);
36
+ else groups.push({ kind: "tools", blocks: [block] });
37
+ } else groups.push({ kind: "block", block });
38
+ }
39
+ return groups;
40
+ }
41
+
42
+ export function summarizeToolGroup(tools: readonly Tool[], locale: string): string {
43
+ const de = locale.startsWith("de");
44
+ const categories = tools.map((tool) => {
45
+ if (tool.name === "todo_write") return de ? "Arbeitsplan aktualisiert" : "Updated working plan";
46
+ if (tool.name.startsWith("code_")) return de ? "Mit Code gearbeitet" : "Worked with code";
47
+ if (tool.presentation?.kind === "capability" || tool.name === "read_cloud_resource")
48
+ return de ? "Cloud-Integration verwendet" : "Used Cloud integration";
49
+ if (["read_file", "list_files", "view_image"].includes(tool.name)) return de ? "Dateien gelesen" : "Read files";
50
+ if (tool.name === "write_file") return de ? "Dateien geschrieben" : "Wrote files";
51
+ if (["load_skill", "load_tools", "search_tools"].includes(tool.name))
52
+ return de ? "Werkzeuge und Wissen geladen" : "Loaded tools and guidance";
53
+ if (tool.name.startsWith("web_") || tool.name === "fetch_file") return de ? "Im Web recherchiert" : "Searched the web";
54
+ return de ? "Werkzeuge verwendet" : "Used tools";
55
+ });
56
+ const errors = tools.filter(isFailedTool).length;
57
+ return [...new Set(categories)].join(", ") + (errors ? ` · ${errors} ${de ? "fehlgeschlagen" : "failed"}` : "");
58
+ }
@@ -0,0 +1,25 @@
1
+ import type { AccessSubject } from "../server/services/access";
2
+ import { hasBillableAiPricing } from "../shared/ai-costs";
3
+ import type { AiChatQuotaSnapshot } from "../shared/ai-quotas";
4
+ import { listAssistantAiModels } from "./assistant-models";
5
+ import { aiQuotas } from "./quotas";
6
+ import { readAiSettingsState } from "./settings";
7
+
8
+ /** Own allowances only. Grant identities and unavailable model profiles stay private. */
9
+ export async function getAiChatQuotas(subject: AccessSubject): Promise<AiChatQuotaSnapshot> {
10
+ const config = await aiQuotas.config();
11
+ if (!config.enabled) return { enabled: false, balances: [] };
12
+ const models = await listAssistantAiModels(subject);
13
+ const snapshot = await aiQuotas.snapshot(subject);
14
+ const profiles = (await readAiSettingsState()).profiles;
15
+ return {
16
+ enabled: snapshot.enabled,
17
+ unit: config.unit ?? "EUR",
18
+ unlimitedModels: models
19
+ .filter((model) => !hasBillableAiPricing(profiles.find((profile) => profile.id === model.id)?.pricing))
20
+ .map((model) => model.id),
21
+ balances: snapshot.balances
22
+ .filter((b) => b.scope === "*" || models.some((m) => m.id === b.scope))
23
+ .map(({ sources: _sources, sourceDetails: _sourceDetails, ...balance }) => balance),
24
+ };
25
+ }
@@ -1,24 +1,24 @@
1
+ import { type TaskGrant, TaskGrantsSchema } from "../services/mandates";
2
+
3
+ export { TaskGrantsSchema as ChatTaskGrantsSchema };
4
+
1
5
  import { dates } from "@k2b/stdlib";
2
- import {
3
- type AiChatTask,
4
- type AiChatTaskOccurrence,
5
- type AiChatTaskOccurrenceState,
6
- type AiChatTaskSchedule,
7
- type AiChatTaskState,
8
- } from "./chat-tasks";
9
- import { AI_SHORT_ID_PATTERN } from "./short-id";
6
+ import { z } from "zod";
10
7
  import { coreSettings } from "../services";
11
8
  import { normalizeWorkflowSchedule } from "../workflows/runtime";
12
- import { z } from "zod";
9
+ import type { AiChatTask, AiChatTaskOccurrence, AiChatTaskOccurrenceState, AiChatTaskSchedule, AiChatTaskState } from "./chat-tasks";
10
+ import { AI_SHORT_ID_PATTERN } from "./short-id";
13
11
 
14
12
  export const ChatTaskIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character scheduled task ID.");
15
13
  export const ChatTaskOccurrenceIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character task occurrence ID.");
16
14
  export const AiConversationIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character AI conversation ID.");
17
15
 
18
16
  export type AiChatTaskView = {
17
+ nextRunAt?: string | null;
19
18
  id: string;
20
19
  chatId: string;
21
20
  chatTitle: string;
21
+ grants: TaskGrant[];
22
22
  prompt: string;
23
23
  schedule: AiChatTaskSchedule;
24
24
  timezone: string;
@@ -35,6 +35,7 @@ export type AiChatTaskOccurrenceView = {
35
35
  trigger: "scheduled" | "manual";
36
36
  state: AiChatTaskOccurrenceState;
37
37
  error: string | null;
38
+ resultText: string | null;
38
39
  createdAt: string;
39
40
  startedAt: string | null;
40
41
  completedAt: string | null;
@@ -44,6 +45,7 @@ export const toAiChatTaskView = (task: AiChatTask): AiChatTaskView => ({
44
45
  id: task.shortId,
45
46
  chatId: task.chatId,
46
47
  chatTitle: task.chatTitle,
48
+ grants: task.grants,
47
49
  prompt: task.prompt,
48
50
  schedule: task.schedule,
49
51
  timezone: task.timezone,
@@ -60,6 +62,7 @@ export const toAiChatTaskOccurrenceView = (occurrence: AiChatTaskOccurrence, tas
60
62
  trigger: occurrence.trigger,
61
63
  state: occurrence.state,
62
64
  error: occurrence.error,
65
+ resultText: occurrence.resultText,
63
66
  createdAt: occurrence.createdAt,
64
67
  startedAt: occurrence.startedAt,
65
68
  completedAt: occurrence.completedAt,
@@ -92,9 +95,10 @@ export const chatTaskCreateFingerprint = (input: {
92
95
  prompt: string;
93
96
  schedule: z.infer<typeof ChatTaskScheduleInputSchema>;
94
97
  timezone?: string;
98
+ grants?: TaskGrant[];
95
99
  }): string =>
96
100
  new Bun.CryptoHasher("sha256")
97
- .update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone ?? null]))
101
+ .update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone ?? null, input.grants ?? []]))
98
102
  .digest("hex");
99
103
 
100
104
  export const normalizeChatTaskSchedule = async (