@k2b/cloud 0.27.0 → 0.28.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 (79) hide show
  1. package/package.json +2 -2
  2. package/src/ai/browser-code-contracts.ts +33 -63
  3. package/src/ai/browser.ts +1 -0
  4. package/src/ai/chat/blocks.tsx +16 -7
  5. package/src/ai/chat/live-turn.browser-harness.tsx +6 -3
  6. package/src/ai/chat/message-actions.tsx +123 -101
  7. package/src/ai/chat/message-utils.ts +30 -1
  8. package/src/ai/chat/messages.ts +198 -0
  9. package/src/ai/chat/presentation.tsx +66 -20
  10. package/src/ai/chat/tool-groups.ts +1 -1
  11. package/src/ai/chat/turn-error.ts +21 -0
  12. package/src/ai/chat/turn-view.tsx +55 -4
  13. package/src/ai/chat/user-message.tsx +19 -14
  14. package/src/ai/chat/visual-tools.tsx +1 -1
  15. package/src/ai/client/controller.ts +23 -6
  16. package/src/ai/code-mode-skill.ts +21 -25
  17. package/src/ai/code-runtime-tools.ts +5 -1
  18. package/src/ai/code-source-contracts.ts +2 -2
  19. package/src/ai/data-analysis-skill.ts +3 -3
  20. package/src/ai/default-tools.ts +15 -16
  21. package/src/ai/executor.ts +135 -59
  22. package/src/ai/index.ts +2 -0
  23. package/src/ai/open-tool-calls.ts +87 -0
  24. package/src/ai/routes.ts +6 -0
  25. package/src/ai/run-timeout.ts +4 -5
  26. package/src/ai/runtime.ts +8 -0
  27. package/src/ai/skill-seeds.ts +4 -4
  28. package/src/ai/store.ts +46 -14
  29. package/src/ai/system-prompt.ts +4 -21
  30. package/src/ai/turn-failure.ts +100 -0
  31. package/src/ai/turn-policy.ts +2 -1
  32. package/src/ai/types.ts +28 -1
  33. package/src/api/admin-outgoing-mail.ts +185 -5
  34. package/src/browser/FileChooser.tsx +11 -0
  35. package/src/browser/file-chooser-messages.ts +2 -0
  36. package/src/cli/admin/index.ts +3 -1
  37. package/src/cli/admin/notifications.ts +6 -0
  38. package/src/cli/admin/outgoing-mail.ts +104 -7
  39. package/src/contracts/outgoing-mail.ts +189 -1
  40. package/src/services/help/store.ts +2 -1
  41. package/src/services/index.ts +11 -1
  42. package/src/services/notifications/batches.ts +139 -79
  43. package/src/services/notifications/channels.ts +33 -13
  44. package/src/services/notifications/dispatcher.ts +57 -5
  45. package/src/services/notifications/email-frame.fixture.html +51 -0
  46. package/src/services/notifications/{email.ts → email-frame.ts} +12 -35
  47. package/src/services/notifications/email-mail.ts +101 -0
  48. package/src/services/notifications/index.ts +49 -36
  49. package/src/services/notifications/observability.ts +9 -1
  50. package/src/services/notifications/platform.ts +1 -1
  51. package/src/services/notifications/runtime.ts +9 -3
  52. package/src/services/outgoing-mail/admin.ts +84 -0
  53. package/src/services/outgoing-mail/attachments.ts +135 -0
  54. package/src/services/outgoing-mail/bulk.ts +11 -0
  55. package/src/services/outgoing-mail/dispatcher.ts +231 -0
  56. package/src/services/outgoing-mail/drain.ts +60 -0
  57. package/src/services/outgoing-mail/enqueue.ts +180 -0
  58. package/src/services/outgoing-mail/index.ts +103 -10
  59. package/src/services/outgoing-mail/message.ts +14 -0
  60. package/src/services/outgoing-mail/messages.ts +431 -0
  61. package/src/services/outgoing-mail/retention.ts +48 -0
  62. package/src/services/outgoing-mail/runtime.ts +72 -0
  63. package/src/services/outgoing-mail/send.ts +130 -0
  64. package/src/services/outgoing-mail/store.ts +115 -4
  65. package/src/services/outgoing-mail/sync.ts +39 -0
  66. package/src/services/outgoing-mail/transport.ts +5 -1
  67. package/src/services/pdf/markdown.ts +22 -4
  68. package/src/services/postgres.ts +15 -0
  69. package/src/services/settings/core-settings.ts +18 -0
  70. package/src/shared/ai-platform-prompt.ts +48 -4
  71. package/src/shared/markdown/extensions/links.ts +28 -20
  72. package/src/shared/markdown/index.ts +14 -5
  73. package/src/shared/markdown/shared.ts +0 -7
  74. package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
  75. package/src/ssr/platform-messages.ts +1 -1
  76. package/src/styles/effects.css +15 -17
  77. package/src/styles/tokens.css +2 -0
  78. package/src/styles/utilities-markdown-editor.css +4 -39
  79. package/src/styles/utilities-markdown-table.css +14 -17
@@ -13,11 +13,14 @@ import {
13
13
  userVisibleTextFromMessage,
14
14
  vfsAttachmentsFromMessage,
15
15
  } from "./message-utils";
16
+ import { aiChatMessages } from "./messages";
16
17
 
17
18
  const openModifyRetryDialog = (
18
19
  entry: AiStoredMessage,
19
20
  onRetryMessage: (entry: AiStoredMessage, input?: AiRetryMessageInput) => void | Promise<void>,
21
+ locale: string,
20
22
  ) => {
23
+ const t = aiChatMessages(locale);
21
24
  void dialogCore.open<void>((close) => {
22
25
  const [draft, setDraft] = createSignal(userVisibleTextFromMessage(entry.message));
23
26
  const content = () => userContentWithEditedVisibleText(entry.message, draft());
@@ -34,11 +37,11 @@ const openModifyRetryDialog = (
34
37
 
35
38
  return (
36
39
  <PanelDialog>
37
- <PanelDialog.Header title="Edit and try again" icon="ti ti-pencil" close={close} />
40
+ <PanelDialog.Header title={t.retryTitle} icon="ti ti-pencil" close={close} />
38
41
  <PanelDialog.Body>
39
42
  <TextInput
40
- label="Prompt"
41
- description="Attachments from the original message stay attached."
43
+ label={t.retryPrompt}
44
+ description={t.retryPromptHint}
42
45
  multiline
43
46
  lines={8}
44
47
  value={draft}
@@ -47,18 +50,18 @@ const openModifyRetryDialog = (
47
50
  />
48
51
  <Show when={retryMutation.error()}>
49
52
  <p class="text-xs text-red-600 dark:text-red-400" role="alert">
50
- Could not retry this message. Your changes are still here.
53
+ {t.retryFailed}
51
54
  </p>
52
55
  </Show>
53
56
  </PanelDialog.Body>
54
57
  <PanelDialog.Footer>
55
58
  <div class="ml-auto flex items-center gap-2">
56
59
  <Button type="button" variant="secondary" size="sm" onClick={() => close()}>
57
- Cancel
60
+ {t.cancel}
58
61
  </Button>
59
62
  <Button type="button" variant="ai" size="sm" disabled={!canRetry()} onClick={retry}>
60
63
  <i class={`ti ${retryMutation.loading() ? "ti-loader-2 animate-spin" : "ti-refresh"}`} aria-hidden="true" />
61
- {retryMutation.loading() ? "Trying again" : "Try again"}
64
+ {retryMutation.loading() ? t.retrying : t.actionTryAgain}
62
65
  </Button>
63
66
  </div>
64
67
  </PanelDialog.Footer>
@@ -93,38 +96,39 @@ export const aiUserMessageAttachments = (entry: AiStoredMessage, actions: AiChat
93
96
  })),
94
97
  ];
95
98
 
96
- export const createAiUserMessageActions = (entry: AiStoredMessage, actions: AiChatActions): ChatAction[] => {
99
+ export const createAiUserMessageActions = (entry: AiStoredMessage, actions: AiChatActions, locale: string): ChatAction[] => {
100
+ const t = aiChatMessages(locale);
97
101
  const result: ChatAction[] = [];
98
102
  const copyText = copyTextFromMessage(entry.message);
99
103
 
100
104
  if (copyText) {
101
- result.push({ id: "copy", label: "Copy", icon: "ti ti-copy", copyText });
105
+ result.push({ id: "copy", label: t.actionCopy, icon: "ti ti-copy", copyText });
102
106
  }
103
107
  if (!entry.compactedAt && actions.onRetryMessage) {
104
108
  result.push(
105
109
  {
106
110
  id: "retry",
107
- label: "Try again",
111
+ label: t.actionTryAgain,
108
112
  icon: "ti ti-refresh",
109
113
  onSelect: () => actions.onRetryMessage?.(entry, { mode: "retry" }),
110
114
  },
111
115
  {
112
116
  id: "details",
113
- label: "More detailed",
117
+ label: t.actionMoreDetailed,
114
118
  icon: "ti ti-list-details",
115
119
  onSelect: () => actions.onRetryMessage?.(entry, { mode: "details" }),
116
120
  },
117
121
  {
118
122
  id: "concise",
119
- label: "More concise",
123
+ label: t.actionMoreConcise,
120
124
  icon: "ti ti-align-left",
121
125
  onSelect: () => actions.onRetryMessage?.(entry, { mode: "concise" }),
122
126
  },
123
127
  {
124
128
  id: "edit",
125
- label: "Edit prompt",
129
+ label: t.actionEditPrompt,
126
130
  icon: "ti ti-pencil",
127
- onSelect: () => openModifyRetryDialog(entry, actions.onRetryMessage!),
131
+ onSelect: () => openModifyRetryDialog(entry, actions.onRetryMessage!, locale),
128
132
  },
129
133
  );
130
134
  }
@@ -136,12 +140,13 @@ export const aiSteerMessageText = (block: Extract<AiTurnBlock, { kind: "steer_me
136
140
  export const createAiSteerMessageActions = (
137
141
  block: Extract<AiTurnBlock, { kind: "steer_message" }>,
138
142
  actions: AiChatActions,
143
+ locale: string,
139
144
  ): ChatAction[] => {
140
145
  if (block.status !== "failed" || !actions.onRetrySteer) return [];
141
146
  return [
142
147
  {
143
148
  id: "retry-steer",
144
- label: "Retry guidance",
149
+ label: aiChatMessages(locale).actionRetryGuidance,
145
150
  icon: "ti ti-refresh",
146
151
  onSelect: () => actions.onRetrySteer?.(block),
147
152
  },
@@ -228,7 +228,7 @@ export function CloudSurveyBlock(props: {
228
228
  </Show>
229
229
  </div>
230
230
  </div>
231
- <footer class="flex min-h-12 items-center gap-3 border-t border-[var(--k2b-border)] bg-[var(--k2b-surface-subtle)] px-4 py-2.5">
231
+ <footer class="flex min-h-12 items-center gap-3 px-4 pb-3">
232
232
  <Show when={submitted()}>
233
233
  <span class="inline-flex items-center gap-1.5 text-xs font-medium text-secondary">
234
234
  <i class="ti ti-check text-[var(--k2b-ai-accent)]" aria-hidden="true" />
@@ -94,13 +94,26 @@ const isCurrentStreamSession = (current: AiStreamSession | null, candidate: AiSt
94
94
 
95
95
  const DEFAULT_RUN_ERROR = "Assistant response failed.";
96
96
 
97
- const conversationRunError = (conversation: AiConversation | null | undefined): string | null =>
98
- conversation?.runStatus === "failed" ? conversation.runError?.trim() || DEFAULT_RUN_ERROR : null;
97
+ /** The failure of the chat's latest turn, and that turn when known. */
98
+ type AiRunError = { message: string; turnId: string | null };
99
99
 
100
- const runErrorFromEvent = (event: AiStreamEvent, activeTurnId: string | null | undefined): string | null | undefined => {
100
+ const conversationRunError = (conversation: AiConversation | null | undefined): AiRunError | null =>
101
+ conversation?.runStatus === "failed"
102
+ ? { message: conversation.runError?.trim() || DEFAULT_RUN_ERROR, turnId: conversation.runTurnId ?? null }
103
+ : null;
104
+
105
+ /**
106
+ * Whether the failed turn shows its failure itself: its last message carries the reason, and the timeline words it in
107
+ * a notice at the turn's end. The run error then stays out of `error()`, so it does not appear twice. A turn without a
108
+ * recorded reason, such as a failed compaction or a turn from an earlier release, still reports it there.
109
+ */
110
+ const turnShowsError = (messages: readonly AiStoredMessage[], turnId: string | null): boolean =>
111
+ turnId !== null && messages.some((message) => message.loopId === turnId && message.meta?.turnError);
112
+
113
+ const runErrorFromEvent = (event: AiStreamEvent, activeTurnId: string | null | undefined): AiRunError | null | undefined => {
101
114
  if (event.type === "state") return conversationRunError(event.conversation);
102
115
  if (event.type !== "turn_finished" || event.turnId !== activeTurnId) return undefined;
103
- return event.status === "failed" ? event.error?.trim() || DEFAULT_RUN_ERROR : null;
116
+ return event.status === "failed" ? { message: event.error?.trim() || DEFAULT_RUN_ERROR, turnId: event.turnId } : null;
104
117
  };
105
118
 
106
119
  export type CreateAiChatControllerOptions = {
@@ -168,7 +181,7 @@ const errorText = (error: unknown, fallback: string): string =>
168
181
  export const createAiChatController = (options: CreateAiChatControllerOptions) => {
169
182
  const [activeConversationId, setActiveConversationIdSignal] = createSignal<string | null>(options.initialConversationId ?? null);
170
183
  const [globalError, setGlobalError] = createSignal<string | null>(options.initialError ?? null);
171
- const [runError, setRunError] = createSignal<string | null>(conversationRunError(options.initialDetail?.conversation));
184
+ const [runError, setRunError] = createSignal<AiRunError | null>(conversationRunError(options.initialDetail?.conversation));
172
185
  const [runStatusRaw, setRunStatusRaw] = createSignal<AiChatRunStatus | null>(null);
173
186
  const [streamStatus, setStreamStatus] = createSignal<AiStreamStatus>("idle");
174
187
  const initialProjection = options.initialDetail ? detailToProjection(options.initialDetail) : emptyProjection();
@@ -250,7 +263,10 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
250
263
  const conversationId = activeConversationId();
251
264
  if (conversationId) openStream(conversationId);
252
265
  };
253
- const error = () => globalError() ?? runError();
266
+ const error = () => {
267
+ const run = runError();
268
+ return globalError() ?? (run && !turnShowsError(state.messages, run.turnId) ? run.message : null);
269
+ };
254
270
 
255
271
  const activeTurn = () => state.activeTurn;
256
272
  const messages = createMemo(() => messagesWithPendingSend(visibleMessages(state), pendingSends()[activeConversationId() ?? ""]));
@@ -1415,6 +1431,7 @@ export const __aiControllerTest = {
1415
1431
  completeFrontendToolBlock,
1416
1432
  conversationRunError,
1417
1433
  failSteerBlock,
1434
+ turnShowsError,
1418
1435
  projectionForConversationOpen,
1419
1436
  reconcileSteerBlocks,
1420
1437
  runErrorFromEvent,