@k2b/cloud 0.27.0 → 0.29.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 (155) hide show
  1. package/package.json +7 -5
  2. package/src/_internal/cli-plugins.ts +18 -0
  3. package/src/_internal/define-app.ts +21 -5
  4. package/src/_internal/registry-validation.ts +30 -1
  5. package/src/_internal/runtime-context.ts +1 -0
  6. package/src/access/PermissionEditor.tsx +11 -4
  7. package/src/ai/assistant-models.ts +25 -4
  8. package/src/ai/audio-tool.ts +3 -10
  9. package/src/ai/browser-code-contracts.ts +33 -63
  10. package/src/ai/browser.ts +1 -0
  11. package/src/ai/capabilities.ts +35 -9
  12. package/src/ai/chat/blocks.tsx +113 -24
  13. package/src/ai/chat/builtin-tools.tsx +3 -1
  14. package/src/ai/chat/composer-adapter.ts +8 -0
  15. package/src/ai/chat/live-turn.browser-harness.tsx +15 -3
  16. package/src/ai/chat/message-actions.tsx +124 -102
  17. package/src/ai/chat/message-utils.ts +57 -6
  18. package/src/ai/chat/messages.ts +214 -0
  19. package/src/ai/chat/presentation.tsx +67 -28
  20. package/src/ai/chat/tool-groups.ts +1 -1
  21. package/src/ai/chat/tool-names.ts +157 -0
  22. package/src/ai/chat/turn-error.ts +21 -0
  23. package/src/ai/chat/turn-layout.ts +12 -0
  24. package/src/ai/chat/turn-view.tsx +69 -10
  25. package/src/ai/chat/user-message.tsx +19 -14
  26. package/src/ai/chat/visual-tools.tsx +1 -1
  27. package/src/ai/chat-tasks.ts +16 -3
  28. package/src/ai/client/controller.ts +23 -6
  29. package/src/ai/code-mode-skill.ts +21 -25
  30. package/src/ai/code-runtime-tools.ts +5 -1
  31. package/src/ai/code-source-contracts.ts +2 -2
  32. package/src/ai/compaction.ts +22 -3
  33. package/src/ai/data-analysis-skill.ts +3 -3
  34. package/src/ai/default-tools.ts +23 -17
  35. package/src/ai/executor.ts +262 -99
  36. package/src/ai/file-media-type.ts +4 -0
  37. package/src/ai/files-store.ts +17 -10
  38. package/src/ai/http.ts +11 -10
  39. package/src/ai/index.ts +3 -0
  40. package/src/ai/memories.ts +2 -0
  41. package/src/ai/memory-learning.ts +59 -5
  42. package/src/ai/memory-workflow-evidence.ts +35 -10
  43. package/src/ai/open-tool-calls.ts +87 -0
  44. package/src/ai/projects-routes.ts +5 -1
  45. package/src/ai/projects.ts +5 -1
  46. package/src/ai/protocol.ts +34 -1
  47. package/src/ai/provider.ts +16 -1
  48. package/src/ai/quota-provider.ts +4 -1
  49. package/src/ai/routes.ts +26 -10
  50. package/src/ai/run-timeout.ts +4 -5
  51. package/src/ai/runtime.ts +8 -0
  52. package/src/ai/settings.ts +54 -17
  53. package/src/ai/skill-seeds.ts +14 -6
  54. package/src/ai/store.ts +102 -34
  55. package/src/ai/structured.ts +2 -1
  56. package/src/ai/system-prompt.ts +9 -22
  57. package/src/ai/transcription.ts +58 -13
  58. package/src/ai/turn-failure.ts +104 -0
  59. package/src/ai/turn-guidance.ts +505 -0
  60. package/src/ai/turn-policy.ts +2 -1
  61. package/src/ai/types.ts +29 -1
  62. package/src/ai/ui.tsx +1 -0
  63. package/src/api/admin-core-settings.ts +16 -1
  64. package/src/api/admin-outgoing-mail.ts +185 -5
  65. package/src/api/me.ts +42 -1
  66. package/src/browser/FileChooser.tsx +82 -295
  67. package/src/browser/FileSaver.tsx +407 -0
  68. package/src/browser/app-badges.ts +16 -0
  69. package/src/browser/choose-files.browser-harness.tsx +22 -6
  70. package/src/browser/choose-files.tsx +23 -36
  71. package/src/browser/file-chooser-messages.ts +66 -0
  72. package/src/browser/file-providers.ts +258 -8
  73. package/src/browser/files.tsx +26 -3
  74. package/src/browser/notifications.ts +37 -1
  75. package/src/browser/provider-folder.tsx +301 -0
  76. package/src/browser/provider-list.ts +39 -0
  77. package/src/browser/save-files.browser-harness.tsx +35 -0
  78. package/src/browser/save-files.tsx +107 -0
  79. package/src/cli/admin/index.ts +3 -1
  80. package/src/cli/admin/notifications.ts +6 -0
  81. package/src/cli/admin/outgoing-mail.ts +104 -7
  82. package/src/contracts/app.ts +10 -0
  83. package/src/contracts/commands.ts +3 -1
  84. package/src/contracts/notification-group.ts +6 -0
  85. package/src/contracts/notification-types.ts +6 -0
  86. package/src/contracts/outgoing-mail.ts +189 -1
  87. package/src/contracts/registry.ts +1 -0
  88. package/src/contracts/user-notifications.ts +74 -0
  89. package/src/server/middleware/request-logger.ts +11 -2
  90. package/src/services/accounts/avatar.ts +5 -2
  91. package/src/services/help/store.ts +2 -1
  92. package/src/services/image-metadata.ts +438 -0
  93. package/src/services/index.ts +12 -1
  94. package/src/services/logging/redaction.ts +50 -0
  95. package/src/services/mandates/index.ts +4 -2
  96. package/src/services/mandates/policy.ts +20 -0
  97. package/src/services/notifications/batches.ts +139 -79
  98. package/src/services/notifications/browser.ts +48 -15
  99. package/src/services/notifications/channels.ts +33 -13
  100. package/src/services/notifications/dispatcher.ts +88 -6
  101. package/src/services/notifications/email-frame.fixture.html +51 -0
  102. package/src/services/notifications/{email.ts → email-frame.ts} +12 -35
  103. package/src/services/notifications/email-mail.ts +101 -0
  104. package/src/services/notifications/index.ts +49 -36
  105. package/src/services/notifications/observability.ts +9 -1
  106. package/src/services/notifications/platform.ts +33 -5
  107. package/src/services/notifications/preview.ts +23 -0
  108. package/src/services/notifications/quiet.ts +153 -0
  109. package/src/services/notifications/runtime.ts +9 -3
  110. package/src/services/notifications/user.ts +6 -0
  111. package/src/services/outgoing-mail/admin.ts +84 -0
  112. package/src/services/outgoing-mail/attachments.ts +135 -0
  113. package/src/services/outgoing-mail/bulk.ts +11 -0
  114. package/src/services/outgoing-mail/dispatcher.ts +231 -0
  115. package/src/services/outgoing-mail/drain.ts +60 -0
  116. package/src/services/outgoing-mail/enqueue.ts +180 -0
  117. package/src/services/outgoing-mail/index.ts +103 -10
  118. package/src/services/outgoing-mail/message.ts +14 -0
  119. package/src/services/outgoing-mail/messages.ts +431 -0
  120. package/src/services/outgoing-mail/retention.ts +48 -0
  121. package/src/services/outgoing-mail/runtime.ts +72 -0
  122. package/src/services/outgoing-mail/send.ts +130 -0
  123. package/src/services/outgoing-mail/store.ts +115 -4
  124. package/src/services/outgoing-mail/sync.ts +39 -0
  125. package/src/services/outgoing-mail/transport.ts +5 -1
  126. package/src/services/pdf/markdown.ts +22 -4
  127. package/src/services/pdf/template-preview.ts +2 -1
  128. package/src/services/postgres.ts +15 -0
  129. package/src/services/settings/app.ts +3 -0
  130. package/src/services/settings/core-settings.ts +18 -0
  131. package/src/services/settings/store.ts +4 -1
  132. package/src/services/weather/geo.ts +6 -2
  133. package/src/shared/ai-platform-prompt.ts +48 -4
  134. package/src/shared/markdown/extensions/links.ts +28 -20
  135. package/src/shared/markdown/index.ts +14 -5
  136. package/src/shared/markdown/shared.ts +0 -7
  137. package/src/shared/redirect.ts +2 -1
  138. package/src/shared/template-rendering.ts +120 -7
  139. package/src/ssr/AppBadge.tsx +30 -0
  140. package/src/ssr/AppLaunchpadPanel.tsx +47 -26
  141. package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
  142. package/src/ssr/IslandError.tsx +33 -0
  143. package/src/ssr/Layout.tsx +3 -0
  144. package/src/ssr/LayoutHelp.tsx +4 -2
  145. package/src/ssr/RailApps.island.tsx +9 -2
  146. package/src/ssr/app-badges.ts +93 -0
  147. package/src/ssr/platform-messages.ts +1 -1
  148. package/src/ssr/rail-messages.ts +4 -0
  149. package/src/ssr/rail-navigation.ts +2 -0
  150. package/src/styles/effects.css +38 -17
  151. package/src/styles/file-chooser.css +25 -0
  152. package/src/styles/tokens.css +2 -0
  153. package/src/styles/utilities-markdown-editor.css +4 -39
  154. package/src/styles/utilities-markdown-table.css +14 -17
  155. package/src/styles/utilities-navigation.css +41 -0
@@ -1,4 +1,5 @@
1
1
  import { i18n } from "@k2b/stdlib";
2
+ import type { AiTurnError } from "../types";
2
3
 
3
4
  const messages = i18n.define({
4
5
  baseLocale: "en",
@@ -21,6 +22,25 @@ const messages = i18n.define({
21
22
  thinking: "Thinking",
22
23
  showReasoning: "Show reasoning",
23
24
  reconnecting: "Reconnecting",
25
+ turnErrorTitle: "The answer was interrupted.",
26
+ turnErrorReason: ({ code, limitMinutes }: AiTurnError) => {
27
+ if (code === "model_unavailable") return "The model service did not answer.";
28
+ if (code === "provider_stopped") return "The model provider ended this answer early.";
29
+ if (code === "quota_exhausted") return "Your AI usage limit for this period is reached. You can continue once it resets.";
30
+ if (code === "context_full") return "This chat is too long for the model. Start a new chat to continue; this one stays as it is.";
31
+ if (code === "time_limit")
32
+ return limitMinutes ? `The run time limit of ${limitMinutes} minutes was reached.` : "The run time limit was reached.";
33
+ if (code === "step_limit") return "The model reached its step limit for one answer.";
34
+ if (code === "wait_expired") return "The approval or answer did not arrive in time.";
35
+ if (code === "interrupted") return "The work was interrupted several times and could not be resumed.";
36
+ if (code === "not_allowed")
37
+ return "This chat may no longer use its model, project, or apps. Choose another model or ask your administrator.";
38
+ return "Something went wrong.";
39
+ },
40
+ turnErrorKept: "The results so far are kept.",
41
+ turnErrorContinueHint: "Send a new message to continue.",
42
+ continueTurn: "Continue",
43
+ continueTurnMessage: "Continue where you left off.",
24
44
  byteRange: ({ start, end }: { start: string; end: string }) => `Bytes ${start}–${end}`,
25
45
  worked: "Worked",
26
46
  workedFor: ({ duration }: { duration: string }) => `Worked ${duration}`,
@@ -52,6 +72,7 @@ const messages = i18n.define({
52
72
  stepWritingCode: "Writing code",
53
73
  stepRunningCode: "Running code",
54
74
  stepCheckingApp: "Checking the app",
75
+ stepCheckingRun: "Checking the run",
55
76
  stepCode: "Working with code",
56
77
  stepPlan: "Updating the working plan",
57
78
  stepPdf: "Creating a PDF",
@@ -68,6 +89,11 @@ const messages = i18n.define({
68
89
  stepFailed: "failed",
69
90
  stepRejected: "rejected",
70
91
  stepWaiting: "waiting",
92
+ checkPassed: "passed",
93
+ checkNotPassed: "not passed",
94
+ checkFindings: ({ count }: { count: number }) => (count === 1 ? "1 finding" : `${count} findings`),
95
+ checkWarnings: ({ count }: { count: number }) => (count === 1 ? "1 warning" : `${count} warnings`),
96
+ openImage: ({ name }: { name: string }) => `Open image ${name}`,
71
97
  steered: "Conversation steered",
72
98
  reasoning: "Reasoning",
73
99
  groupPlan: "Updated working plan",
@@ -187,6 +213,87 @@ const messages = i18n.define({
187
213
  calculate: "Calculate",
188
214
  inspectImage: ({ name }: { name: string }) => `Inspect image: ${name}`,
189
215
  readResource: ({ ref }: { ref: string }) => `Read resource: ${ref}`,
216
+ actionHelpful: "Helpful",
217
+ actionHelpfulRemove: "Remove positive feedback",
218
+ actionImprove: "Needs improvement",
219
+ actionImproveRemove: "Remove negative feedback",
220
+ actionMessageInfo: "Response details",
221
+ actionCopy: "Copy",
222
+ actionFork: "Continue in a new chat",
223
+ actionTryAgain: "Try again",
224
+ actionMoreDetailed: "More detailed",
225
+ actionMoreConcise: "More concise",
226
+ actionEditPrompt: "Edit and try again",
227
+ actionRetryGuidance: "Send guidance again",
228
+ cancel: "Cancel",
229
+ forkTitle: "Continue in a new chat",
230
+ forkConfirm: "Create chat",
231
+ forkName: "Chat name",
232
+ forkDefaultName: "Continued chat",
233
+ retryTitle: "Edit and try again",
234
+ retryPrompt: "Message",
235
+ retryPromptHint: "Attachments from the original message stay attached.",
236
+ retryFailed: "Could not try again. Your changes are still here.",
237
+ retrying: "Trying again",
238
+ feedbackTitle: "Help improve this response",
239
+ feedbackIntro: "What should be better? Choose all that apply or leave a short note.",
240
+ feedbackDetails: "Anything else",
241
+ feedbackDetailsHint: "Optional. Please leave out confidential information.",
242
+ feedbackSend: "Send feedback",
243
+ feedbackIncorrect: "Incorrect",
244
+ feedbackIncorrectHint: "Facts or conclusions were wrong.",
245
+ feedbackIgnoredRequest: "Did not do what I asked",
246
+ feedbackIgnoredRequestHint: "Important instructions were missed.",
247
+ feedbackIncomplete: "Incomplete",
248
+ feedbackIncompleteHint: "Important information or work was missing.",
249
+ feedbackWrongApp: "Used the wrong app or tool",
250
+ feedbackWrongAppHint: "Something was missing, unnecessary, or used badly.",
251
+ feedbackSlow: "Too slow",
252
+ feedbackSlowHint: "The answer took too long for the result.",
253
+ feedbackOther: "Something else",
254
+ feedbackOtherHint: "Something else should be better.",
255
+ infoSubtitle: "Technical details of this answer",
256
+ infoDetails: "Details",
257
+ infoProviderModel: "Provider model",
258
+ infoModelProfile: "Model",
259
+ infoLoopId: "Run ID",
260
+ infoFinished: "Ended",
261
+ infoEndCompleted: "Completed",
262
+ infoEndStopped: "Stopped",
263
+ infoEndFailed: "Failed",
264
+ infoEndInterrupted: "Interrupted",
265
+ infoEndStepLimit: "Step limit reached",
266
+ infoEndUsageLimit: "Usage limit reached",
267
+ infoEndContextFull: "Context window full",
268
+ infoEndLengthLimit: "Answer length limit reached",
269
+ infoCreated: "Created",
270
+ infoUnknown: "Unknown",
271
+ infoLegacy: "Older message",
272
+ infoTiming: "Time",
273
+ infoWorked: "Worked",
274
+ infoWorkedHint: "writing and tools",
275
+ infoGeneration: "Writing",
276
+ infoToolTime: "Tools",
277
+ infoWaiting: "Waiting for you",
278
+ infoWaitingHint: "approvals and answers",
279
+ infoWallClock: "Total time",
280
+ infoSpeed: "Writing speed",
281
+ infoTokensPerSecond: ({ value }: { value: string }) => `${value} tokens/s`,
282
+ infoUsage: "Usage",
283
+ infoInputTokens: "Input tokens",
284
+ infoOutputTokens: "Output tokens",
285
+ infoTotalTokens: "Total tokens",
286
+ infoTools: "Tools",
287
+ infoToolsHint: "Tools this answer used.",
288
+ infoIssueErrors: "errors",
289
+ infoIssueStream: "connection problems",
290
+ infoIssueMalformed: "malformed",
291
+ infoIssueCancelled: "cancelled",
292
+ compactedCount: ({ count, date }: { count: number; date: string }) =>
293
+ `${count === 1 ? "1 message" : `${count} messages`} summarized · ${date}`,
294
+ compactedHint: "From here on, the model sees this summary instead of the messages above.",
295
+ compactedEmpty: "No visible content",
296
+ malformedImage: "This image could not be read. Export it again or choose another file.",
190
297
  },
191
298
  de: {
192
299
  backgroundRun: "Hintergrundlauf",
@@ -207,6 +314,27 @@ const messages = i18n.define({
207
314
  thinking: "Denkt nach",
208
315
  showReasoning: "Denkprozess anzeigen",
209
316
  reconnecting: "Verbindung wird wiederhergestellt",
317
+ turnErrorTitle: "Die Antwort wurde abgebrochen.",
318
+ turnErrorReason: ({ code, limitMinutes }) => {
319
+ if (code === "model_unavailable") return "Der KI-Dienst hat nicht geantwortet.";
320
+ if (code === "provider_stopped") return "Der KI-Anbieter hat diese Antwort vorzeitig beendet.";
321
+ if (code === "quota_exhausted")
322
+ return "Dein KI-Kontingent für diesen Zeitraum ist aufgebraucht. Du kannst weiterarbeiten, sobald es sich erneuert.";
323
+ if (code === "context_full")
324
+ return "Dieser Chat ist zu lang für das Modell. Starte einen neuen Chat, um weiterzuarbeiten; dieser bleibt erhalten.";
325
+ if (code === "time_limit")
326
+ return limitMinutes ? `Das Laufzeitlimit von ${limitMinutes} Minuten ist erreicht.` : "Das Laufzeitlimit ist erreicht.";
327
+ if (code === "step_limit") return "Das Modell hat sein Schrittlimit für eine Antwort erreicht.";
328
+ if (code === "wait_expired") return "Die Freigabe oder Antwort kam nicht rechtzeitig.";
329
+ if (code === "interrupted") return "Die Arbeit wurde mehrmals unterbrochen und konnte nicht fortgesetzt werden.";
330
+ if (code === "not_allowed")
331
+ return "Dieser Chat darf sein Modell, sein Projekt oder seine Apps nicht mehr nutzen. Wähle ein anderes Modell oder wende dich an deine Administration.";
332
+ return "Etwas ist schiefgelaufen.";
333
+ },
334
+ turnErrorKept: "Die bisherigen Ergebnisse bleiben erhalten.",
335
+ turnErrorContinueHint: "Mit einer neuen Nachricht geht es weiter.",
336
+ continueTurn: "Weiterarbeiten",
337
+ continueTurnMessage: "Mach an der Stelle weiter, an der du aufgehört hast.",
210
338
  byteRange: ({ start, end }) => `Bytes ${start}–${end}`,
211
339
  worked: "Gearbeitet",
212
340
  workedFor: ({ duration }) => `${duration} gearbeitet`,
@@ -242,6 +370,7 @@ const messages = i18n.define({
242
370
  stepWritingCode: "Schreibt Code",
243
371
  stepRunningCode: "Führt Code aus",
244
372
  stepCheckingApp: "Prüft die App",
373
+ stepCheckingRun: "Prüft den Lauf",
245
374
  stepCode: "Arbeitet mit Code",
246
375
  stepPlan: "Aktualisiert den Arbeitsplan",
247
376
  stepPdf: "Erstellt ein PDF",
@@ -258,6 +387,11 @@ const messages = i18n.define({
258
387
  stepFailed: "fehlgeschlagen",
259
388
  stepRejected: "abgelehnt",
260
389
  stepWaiting: "wartet",
390
+ checkPassed: "bestanden",
391
+ checkNotPassed: "nicht bestanden",
392
+ checkFindings: ({ count }) => (count === 1 ? "1 Befund" : `${count} Befunde`),
393
+ checkWarnings: ({ count }) => (count === 1 ? "1 Warnung" : `${count} Warnungen`),
394
+ openImage: ({ name }) => `Bild ${name} öffnen`,
261
395
  steered: "Richtung geändert",
262
396
  reasoning: "Überlegungen",
263
397
  groupPlan: "Arbeitsplan aktualisiert",
@@ -377,6 +511,86 @@ const messages = i18n.define({
377
511
  calculate: "Berechnen",
378
512
  inspectImage: ({ name }) => `Bild prüfen: ${name}`,
379
513
  readResource: ({ ref }) => `Ressource lesen: ${ref}`,
514
+ actionHelpful: "Hilfreich",
515
+ actionHelpfulRemove: "Positive Bewertung entfernen",
516
+ actionImprove: "Verbesserungswürdig",
517
+ actionImproveRemove: "Negative Bewertung entfernen",
518
+ actionMessageInfo: "Details zur Antwort",
519
+ actionCopy: "Kopieren",
520
+ actionFork: "In neuem Chat fortsetzen",
521
+ actionTryAgain: "Noch einmal versuchen",
522
+ actionMoreDetailed: "Ausführlicher",
523
+ actionMoreConcise: "Kürzer",
524
+ actionEditPrompt: "Bearbeiten und neu senden",
525
+ actionRetryGuidance: "Hinweis erneut senden",
526
+ cancel: "Abbrechen",
527
+ forkTitle: "In neuem Chat fortsetzen",
528
+ forkConfirm: "Chat erstellen",
529
+ forkName: "Name des Chats",
530
+ forkDefaultName: "Fortgesetzter Chat",
531
+ retryTitle: "Bearbeiten und neu senden",
532
+ retryPrompt: "Nachricht",
533
+ retryPromptHint: "Die Anhänge der ursprünglichen Nachricht bleiben erhalten.",
534
+ retryFailed: "Das hat nicht geklappt. Deine Änderungen sind noch da.",
535
+ retrying: "Wird gesendet",
536
+ feedbackTitle: "Hilf, diese Antwort zu verbessern",
537
+ feedbackIntro: "Was sollte besser sein? Wähle alles Passende aus oder schreib eine kurze Notiz.",
538
+ feedbackDetails: "Sonst noch etwas",
539
+ feedbackDetailsHint: "Optional. Bitte keine vertraulichen Daten angeben.",
540
+ feedbackSend: "Feedback senden",
541
+ feedbackIncorrect: "Falsch",
542
+ feedbackIncorrectHint: "Fakten oder Schlussfolgerungen stimmten nicht.",
543
+ feedbackIgnoredRequest: "Hat nicht getan, worum ich gebeten habe",
544
+ feedbackIgnoredRequestHint: "Wichtige Anweisungen wurden übersehen.",
545
+ feedbackIncomplete: "Unvollständig",
546
+ feedbackIncompleteHint: "Wichtige Informationen oder Arbeitsschritte fehlten.",
547
+ feedbackWrongApp: "Falsche App oder falsches Werkzeug",
548
+ feedbackWrongAppHint: "Etwas fehlte, war unnötig oder wurde falsch genutzt.",
549
+ feedbackSlow: "Zu langsam",
550
+ feedbackSlowHint: "Die Antwort hat für das Ergebnis zu lange gedauert.",
551
+ feedbackOther: "Etwas anderes",
552
+ feedbackOtherHint: "Etwas anderes sollte besser werden.",
553
+ infoSubtitle: "Technische Angaben zu dieser Antwort",
554
+ infoDetails: "Details",
555
+ infoProviderModel: "Modell beim Anbieter",
556
+ infoModelProfile: "Modell",
557
+ infoLoopId: "Durchlauf-ID",
558
+ infoFinished: "Beendet",
559
+ infoEndCompleted: "Abgeschlossen",
560
+ infoEndStopped: "Gestoppt",
561
+ infoEndFailed: "Fehlgeschlagen",
562
+ infoEndInterrupted: "Unterbrochen",
563
+ infoEndStepLimit: "Schrittlimit erreicht",
564
+ infoEndUsageLimit: "Nutzungslimit erreicht",
565
+ infoEndContextFull: "Kontextfenster voll",
566
+ infoEndLengthLimit: "Maximale Antwortlänge erreicht",
567
+ infoCreated: "Erstellt",
568
+ infoUnknown: "Unbekannt",
569
+ infoLegacy: "Ältere Nachricht",
570
+ infoTiming: "Zeit",
571
+ infoWorked: "Gearbeitet",
572
+ infoWorkedHint: "Schreiben und Werkzeuge",
573
+ infoGeneration: "Schreiben",
574
+ infoToolTime: "Werkzeuge",
575
+ infoWaiting: "Wartezeit auf dich",
576
+ infoWaitingHint: "Freigaben und Antworten",
577
+ infoWallClock: "Gesamtdauer",
578
+ infoSpeed: "Schreibtempo",
579
+ infoTokensPerSecond: ({ value }) => `${value} Token/s`,
580
+ infoUsage: "Verbrauch",
581
+ infoInputTokens: "Eingabe-Token",
582
+ infoOutputTokens: "Ausgabe-Token",
583
+ infoTotalTokens: "Token gesamt",
584
+ infoTools: "Werkzeuge",
585
+ infoToolsHint: "Werkzeuge, die diese Antwort genutzt hat.",
586
+ infoIssueErrors: "Fehler",
587
+ infoIssueStream: "Verbindungsprobleme",
588
+ infoIssueMalformed: "fehlerhaft",
589
+ infoIssueCancelled: "abgebrochen",
590
+ compactedCount: ({ count, date }) => `${count === 1 ? "1 Nachricht" : `${count} Nachrichten`} zusammengefasst · ${date}`,
591
+ compactedHint: "Ab hier sieht das Modell diese Zusammenfassung statt der Nachrichten darüber.",
592
+ compactedEmpty: "Kein sichtbarer Inhalt",
593
+ malformedImage: "Dieses Bild konnte nicht gelesen werden. Exportiere es erneut oder wähle eine andere Datei.",
380
594
  },
381
595
  },
382
596
  });
@@ -15,11 +15,12 @@ import {
15
15
  import type { AiActiveTurn } from "../client/projection";
16
16
  import { type AiActiveTurnSegment, type AiTurnBlock, splitActiveTurnBlocks } from "../protocol";
17
17
  import { type AiAssistantTimelineItem, buildAiMessageTimeline } from "../timeline";
18
- import type { AiConversationTimelineEntry, AiStoredMessage } from "../types";
18
+ import type { AiConversationTimelineEntry, AiStoredMessage, AiTurnError } from "../types";
19
19
  import { type AiChatActions, AiChatActionsProvider, createAssistantMessageActions, useAiChatActions } from "./message-actions";
20
20
  import { isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
21
+ import { aiChatMessages } from "./messages";
21
22
  import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
22
- import { type AiTurnLayout, type AiTurnPhase, layoutAiTurn, waitsForUser } from "./turn-layout";
23
+ import { type AiTurnLayout, type AiTurnPhase, layoutAiTurn, storedPhase, waitsForUser } from "./turn-layout";
23
24
  import { TurnNavigator } from "./turn-navigator";
24
25
  import { activeTimelineSeq } from "./turn-navigator-utils";
25
26
  import { type AiTurnDuration, type AiTurnSegment, AiTurnView } from "./turn-view";
@@ -101,15 +102,9 @@ const leadingSteerOpener = (blocks: readonly AssistantBlock[]): string | null =>
101
102
  return opener;
102
103
  };
103
104
 
104
- /** History phase of a finished loop, from how its loop ended. */
105
- const storedPhase = (entries: readonly AiStoredMessage[]): AiTurnPhase => {
106
- const reason = entries.findLast((entry) => entry.loopDoneReason)?.loopDoneReason;
107
- if (!reason || reason === "stop") return "completed";
108
- return reason === "aborted" ? "stopped" : "failed";
109
- };
110
-
111
- /** Responses with a work line or rich places span the message column so their right edges align. */
112
- const isWideLayout = (layout: AiTurnLayout) => layout.showWork || layout.results.length > 0 || layout.actions.length > 0;
105
+ /** Responses with a work line, rich places, or a failure notice span the message column so their right edges align. */
106
+ const isWideLayout = (layout: AiTurnLayout, error?: AiTurnError) =>
107
+ layout.showWork || layout.results.length > 0 || layout.actions.length > 0 || Boolean(error);
113
108
 
114
109
  const isEmptyLayout = (layout: AiTurnLayout) => !isWideLayout(layout) && layout.text.length === 0;
115
110
 
@@ -162,13 +157,49 @@ const storedItems = (
162
157
  actions: AiChatActions,
163
158
  views: TurnViews,
164
159
  locale: string,
160
+ idle: boolean,
165
161
  ): ChatTimelineItem[] => {
166
162
  const timeline = buildAiMessageTimeline([...messages]);
167
163
  const lastItemOfLoop = new Map<string, number>();
164
+ // A failed turn's notice ends its last item, which is its user message when the model never answered.
165
+ const lastEntryOfLoop = new Map<string, number>();
168
166
  timeline.forEach((item, index) => {
169
- if (item.type === "assistant" && item.loopId) lastItemOfLoop.set(item.loopId, index);
167
+ const loopId = item.type === "summary" ? null : item.type === "user" ? item.entry.loopId : item.loopId;
168
+ if (!loopId) return;
169
+ lastEntryOfLoop.set(loopId, index);
170
+ if (item.type === "assistant") lastItemOfLoop.set(loopId, index);
170
171
  });
172
+ const turnErrors = new Map<string, AiTurnError>();
173
+ for (const message of messages) if (message.loopId && message.meta?.turnError) turnErrors.set(message.loopId, message.meta.turnError);
174
+ // Only the chat's newest turn can be continued, and only while no other turn runs.
175
+ const newestLoop = idle ? (messages.at(-1)?.loopId ?? null) : null;
171
176
  const segmentId = createSegmentIds();
177
+ // The model never answered: the notice stands where the live turn showed its progress, under the same id.
178
+ const unansweredNotice = (entry: AiStoredMessage, itemIndex: number): ChatTimelineItem[] => {
179
+ const loopId = entry.loopId;
180
+ const error = loopId && lastEntryOfLoop.get(loopId) === itemIndex ? turnErrors.get(loopId) : undefined;
181
+ if (!loopId || !error) return [];
182
+ const id = segmentId(loopId, "start");
183
+ return [
184
+ {
185
+ kind: "message",
186
+ id,
187
+ role: "assistant",
188
+ createdAt: entry.createdAt,
189
+ class: "ai-chat-message-wide",
190
+ content: views.content({
191
+ id,
192
+ turnId: loopId,
193
+ phase: "failed",
194
+ layout: layoutAiTurn([], { phase: "failed" }),
195
+ earlier: false,
196
+ duration: () => null,
197
+ error,
198
+ continuable: newestLoop === loopId,
199
+ }),
200
+ },
201
+ ];
202
+ };
172
203
  return timeline.flatMap((item, itemIndex): ChatTimelineItem | ChatTimelineItem[] => {
173
204
  if (item.type === "user") {
174
205
  const text = aiUserMessageText(item.entry);
@@ -176,7 +207,7 @@ const storedItems = (
176
207
  if (agentMessage) {
177
208
  const separator = text.indexOf("\n\n");
178
209
  const forwardedText = separator >= 0 ? text.slice(separator + 2) : text;
179
- return {
210
+ const systemItem: ChatTimelineItem = {
180
211
  kind: "message",
181
212
  id: item.id,
182
213
  role: "system",
@@ -208,18 +239,20 @@ const storedItems = (
208
239
  ),
209
240
  anchorId: item.entry.seq,
210
241
  };
242
+ return [systemItem, ...unansweredNotice(item.entry, itemIndex)];
211
243
  }
212
- return {
244
+ const userItem: ChatTimelineItem = {
213
245
  kind: "message",
214
246
  id: item.id,
215
247
  role: "user",
216
248
  createdAt: item.entry.createdAt,
217
249
  content: text ? <p class="whitespace-pre-wrap">{text}</p> : undefined,
218
250
  attachments: aiUserMessageAttachments(item.entry, actions),
219
- actions: createAiUserMessageActions(item.entry, actions),
251
+ actions: createAiUserMessageActions(item.entry, actions, locale),
220
252
  actionDisplay: "menu",
221
253
  anchorId: item.entry.seq,
222
254
  };
255
+ return [userItem, ...unansweredNotice(item.entry, itemIndex)];
223
256
  }
224
257
 
225
258
  if (item.type === "summary") {
@@ -228,16 +261,14 @@ const storedItems = (
228
261
  return {
229
262
  kind: "activity",
230
263
  id: item.id,
231
- label: "Context compacted",
232
- description: count ? `${count} message${count === 1 ? "" : "s"} summarized · ${date}` : date,
264
+ label: aiChatMessages(locale).compacted,
265
+ description: count ? aiChatMessages(locale).compactedCount({ count, date }) : date,
233
266
  icon: "ti ti-brain",
234
267
  tone: "ai",
235
268
  content: (
236
269
  <div>
237
- <p class="mb-1 text-[10px] font-medium uppercase tracking-wide text-dimmed">
238
- The model now sees this summary instead of the messages above
239
- </p>
240
- <p class="whitespace-pre-wrap">{textFromMessage(item.entry.message) || "No visible content"}</p>
270
+ <p class="mb-1 text-dimmed">{aiChatMessages(locale).compactedHint}</p>
271
+ <p class="whitespace-pre-wrap">{textFromMessage(item.entry.message) || aiChatMessages(locale).compactedEmpty}</p>
241
272
  </div>
242
273
  ),
243
274
  };
@@ -245,9 +276,12 @@ const storedItems = (
245
276
 
246
277
  const actionEntry = item.actionEntry?.compactedAt ? null : item.actionEntry;
247
278
  const lastOfLoop = !item.loopId || lastItemOfLoop.get(item.loopId) === itemIndex;
248
- const phase = lastOfLoop ? storedPhase(item.entries) : "completed";
279
+ const turnError = item.loopId && lastOfLoop ? turnErrors.get(item.loopId) : undefined;
280
+ const phase = turnError ? "failed" : lastOfLoop ? storedPhase(item.entries) : "completed";
249
281
  const segments = splitSurveyResults(item.blocks);
250
- if (segments.length === 0) segments.push({ type: "assistant", blocks: [] });
282
+ // A turn that failed after an accepted answer ends with its notice where the live turn showed its progress, below
283
+ // the answer and under the same id, as activeItems() places it.
284
+ if (segments.length === 0 || (turnError && segments.at(-1)?.type === "survey")) segments.push({ type: "assistant", blocks: [] });
251
285
  const lastAssistant = segments.findLastIndex((segment) => segment.type === "assistant");
252
286
  const scheduledTask = item.entries.find((entry) => entry.meta?.scheduledTask)?.meta?.scheduledTask ?? null;
253
287
  let firstSegment = true;
@@ -268,7 +302,8 @@ const storedItems = (
268
302
  : undefined;
269
303
  const task = firstSegment ? scheduledTask : null;
270
304
  firstSegment = false;
271
- if (isEmptyLayout(layout) && !messageActions && !task) return [];
305
+ const error = last ? turnError : undefined;
306
+ if (isEmptyLayout(layout) && !messageActions && !task && !error) return [];
272
307
  const id = item.loopId ? segmentId(item.loopId, segmentOpener) : `${item.id}:${index}`;
273
308
  return [
274
309
  {
@@ -276,7 +311,7 @@ const storedItems = (
276
311
  id,
277
312
  role: "assistant",
278
313
  createdAt: actionEntry?.createdAt ?? item.entries.at(-1)?.createdAt,
279
- class: isWideLayout(layout) ? "ai-chat-message-wide" : undefined,
314
+ class: isWideLayout(layout, error) ? "ai-chat-message-wide" : undefined,
280
315
  content: views.content({
281
316
  id,
282
317
  turnId,
@@ -285,6 +320,8 @@ const storedItems = (
285
320
  earlier: !last,
286
321
  duration: last ? () => ({ workedMs: item.workedMs, waitingMs: null }) : () => null,
287
322
  scheduledTask: task,
323
+ error,
324
+ continuable: Boolean(error) && newestLoop === item.loopId,
288
325
  }),
289
326
  actions: messageActions,
290
327
  actionDisplay: "inline",
@@ -300,6 +337,7 @@ const activeItems = (
300
337
  views: TurnViews,
301
338
  duration: Accessor<AiTurnDuration | null>,
302
339
  reconnecting: boolean,
340
+ locale: string,
303
341
  ): ChatTimelineItem[] => {
304
342
  if (!turn) return [];
305
343
  const segments = splitActiveTurnBlocks(turn.blocks).flatMap((segment): (AiActiveTurnSegment | SurveySegment)[] =>
@@ -340,7 +378,7 @@ const activeItems = (
340
378
  role: "user",
341
379
  status: block.status === "pending" ? "pending" : block.status === "failed" ? "error" : "complete",
342
380
  content: <p class="whitespace-pre-wrap">{aiSteerMessageText(block)}</p>,
343
- actions: createAiSteerMessageActions(block, actions),
381
+ actions: createAiSteerMessageActions(block, actions, locale),
344
382
  actionDisplay: "menu",
345
383
  anchorId: turn.seq,
346
384
  };
@@ -449,8 +487,9 @@ export function createAiChatTimeline(source: AiChatTimelineSource): Accessor<rea
449
487
  // While the stream reconnects or a model call waits for its retry, the live turn says so and its clock stands.
450
488
  const reconnecting = createMemo(() => source.activeTurn()?.providerRetry === true || source.reconnecting?.() === true);
451
489
  const clock = createActiveTurnClock(source, reconnecting);
452
- const stored = createMemo(() => storedItems(source.messages(), actions, views, locale()));
453
- const active = createMemo(() => activeItems(source.activeTurn(), actions, views, clock, reconnecting()));
490
+ const idle = createMemo(() => source.activeTurn() === null);
491
+ const stored = createMemo(() => storedItems(source.messages(), actions, views, locale(), idle()));
492
+ const active = createMemo(() => activeItems(source.activeTurn(), actions, views, clock, reconnecting(), locale()));
454
493
  return createMemo(() => {
455
494
  const items = [...stored(), ...active()];
456
495
  views.retain(new Set(items.map((item) => item.id)));
@@ -12,7 +12,7 @@ export function isFailedTool(tool: Tool): boolean {
12
12
  if (tool.isError || tool.status === "failed") return true;
13
13
  const result = tool.result;
14
14
  return (
15
- ["code_run", "code_inspect", "code_interact"].includes(tool.name) &&
15
+ ["code_run", "code_inspect"].includes(tool.name) &&
16
16
  result !== null &&
17
17
  typeof result === "object" &&
18
18
  "status" in result &&
@@ -0,0 +1,157 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ /**
4
+ * People-facing names of Cloud's built-in Assistant tools, keyed by tool name. Step rows, receipts, and approvals show
5
+ * these; a capability tool brings its own translated title instead. A tool test fails when a built-in tool has no entry.
6
+ */
7
+ const toolNames = i18n.define({
8
+ baseLocale: "en",
9
+ messages: {
10
+ en: {
11
+ calculate: "Calculate",
12
+ card: "Card",
13
+ survey: "Survey",
14
+ text_editor: "Text editor",
15
+ local_bash: "Local Bash",
16
+ todo_write: "Working plan",
17
+ memory: "Memory",
18
+ list_files: "List files",
19
+ read_file: "Read file",
20
+ write_file: "Write file",
21
+ fetch_file: "Download file",
22
+ present: "Deliver file",
23
+ markdown_to_pdf: "Markdown to PDF",
24
+ html_to_pdf: "HTML to PDF",
25
+ view_image: "View image",
26
+ transcribe_audio: "Transcribe audio",
27
+ web_search: "Web search",
28
+ web_extract: "Read web page",
29
+ search_tools: "Search tools",
30
+ load_tools: "Load tools",
31
+ list_apps: "List apps",
32
+ search_help: "Search help",
33
+ read_help: "Read help",
34
+ search_project: "Search project",
35
+ read_project_knowledge: "Read project knowledge",
36
+ read_cloud_resource: "Read Cloud item",
37
+ load_skill: "Load skill",
38
+ search_skills: "Search skills",
39
+ code_run: "Run code",
40
+ code_action: "Run app action",
41
+ code_inspect: "Inspect run",
42
+ code_stop: "Stop run",
43
+ code_open: "Open app",
44
+ code_export: "Save run output",
45
+ code_present: "Show app in chat",
46
+ code_check: "App check",
47
+ code_secret: "Enter secret",
48
+ code_database: "App tables",
49
+ code_files: "List stored files",
50
+ code_file_stat: "File details",
51
+ code_file_copy: "Copy file",
52
+ code_database_export: "Export app database",
53
+ code_manage_read: "Read app details",
54
+ code_delete: "Delete app",
55
+ code_database_read: "Read app database",
56
+ code_database_clear: "Clear app data",
57
+ code_database_reset: "Reset app database",
58
+ code_storage_list: "List app storage",
59
+ code_storage_delete: "Delete from app storage",
60
+ code_access_read: "Read app access",
61
+ code_access_change: "Change app access",
62
+ code_actions: "List app actions",
63
+ code_sql: "Query app database",
64
+ code_versions: "List app versions",
65
+ code_list: "Find apps",
66
+ code_read: "Read app source",
67
+ code_history: "App history",
68
+ code_fork: "Copy app",
69
+ code_publish: "Publish app",
70
+ code_unpublish: "Withdraw app",
71
+ code_restore: "Restore app version",
72
+ code_update: "Update app details",
73
+ code_create: "Create app",
74
+ code_write: "Save app files",
75
+ code_remove: "Remove app file",
76
+ },
77
+ de: {
78
+ calculate: "Rechnen",
79
+ card: "Karte",
80
+ survey: "Umfrage",
81
+ text_editor: "Texteditor",
82
+ local_bash: "Lokale Bash",
83
+ todo_write: "Arbeitsplan",
84
+ memory: "Gedächtnis",
85
+ list_files: "Dateien auflisten",
86
+ read_file: "Datei lesen",
87
+ write_file: "Datei schreiben",
88
+ fetch_file: "Datei herunterladen",
89
+ present: "Datei bereitstellen",
90
+ markdown_to_pdf: "Markdown als PDF",
91
+ html_to_pdf: "HTML als PDF",
92
+ view_image: "Bild ansehen",
93
+ transcribe_audio: "Audio transkribieren",
94
+ web_search: "Websuche",
95
+ web_extract: "Webseite lesen",
96
+ search_tools: "Werkzeuge suchen",
97
+ load_tools: "Werkzeuge laden",
98
+ list_apps: "Apps auflisten",
99
+ search_help: "Hilfe durchsuchen",
100
+ read_help: "Hilfe lesen",
101
+ search_project: "Projekt durchsuchen",
102
+ read_project_knowledge: "Projektwissen lesen",
103
+ read_cloud_resource: "Cloud-Eintrag lesen",
104
+ load_skill: "Skill laden",
105
+ search_skills: "Skills suchen",
106
+ code_run: "Code ausführen",
107
+ code_action: "App-Aktion ausführen",
108
+ code_inspect: "Lauf prüfen",
109
+ code_stop: "Lauf beenden",
110
+ code_open: "App öffnen",
111
+ code_export: "Laufergebnis speichern",
112
+ code_present: "App im Chat zeigen",
113
+ code_check: "App-Prüfung",
114
+ code_secret: "Geheimnis eingeben",
115
+ code_database: "App-Tabellen",
116
+ code_files: "Gespeicherte Dateien auflisten",
117
+ code_file_stat: "Dateidetails",
118
+ code_file_copy: "Datei kopieren",
119
+ code_database_export: "App-Datenbank exportieren",
120
+ code_manage_read: "App-Details lesen",
121
+ code_delete: "App löschen",
122
+ code_database_read: "App-Datenbank lesen",
123
+ code_database_clear: "App-Daten leeren",
124
+ code_database_reset: "App-Datenbank zurücksetzen",
125
+ code_storage_list: "App-Speicher auflisten",
126
+ code_storage_delete: "Aus App-Speicher löschen",
127
+ code_access_read: "App-Zugriff lesen",
128
+ code_access_change: "App-Zugriff ändern",
129
+ code_actions: "App-Aktionen auflisten",
130
+ code_sql: "App-Datenbank abfragen",
131
+ code_versions: "App-Versionen auflisten",
132
+ code_list: "Apps suchen",
133
+ code_read: "App-Quelltext lesen",
134
+ code_history: "App-Verlauf",
135
+ code_fork: "App kopieren",
136
+ code_publish: "App veröffentlichen",
137
+ code_unpublish: "App zurückziehen",
138
+ code_restore: "App-Version wiederherstellen",
139
+ code_update: "App-Details ändern",
140
+ code_create: "App erstellen",
141
+ code_write: "App-Dateien speichern",
142
+ code_remove: "App-Datei entfernen",
143
+ },
144
+ },
145
+ });
146
+
147
+ /** Older chats call the visual tools by their `cloud_` names. */
148
+ const ALIASES: Record<string, string> = { cloud_card: "card", cloud_survey: "survey", cloud_text_editor: "text_editor" };
149
+
150
+ /** The localized name of a built-in tool, or undefined for a tool Cloud does not ship. */
151
+ export const aiBuiltinToolName = (name: string, locale: string): string | undefined => {
152
+ const t: Record<string, string> = toolNames.resolve([locale]).t;
153
+ const key = ALIASES[name] ?? name;
154
+ return Object.hasOwn(t, key) ? t[key] : undefined;
155
+ };
156
+
157
+ export const checkAiToolNames = () => toolNames.check();