@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/cloud",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "repository": {
@@ -100,7 +100,7 @@
100
100
  "@tailwindcss/typography": "0.5.20",
101
101
  "@k2b/nessi": "0.15.0",
102
102
  "@k2b/ssr": "0.14.0",
103
- "@k2b/ui": "0.17.0",
103
+ "@k2b/ui": "0.18.0",
104
104
  "@k2b/stdlib": "0.27.0",
105
105
  "@k2b/sync": "7.0.0",
106
106
  "@nats-io/jetstream": "3.4.0",
@@ -10,20 +10,20 @@ export const CODE_RUNTIME_TOOL_NAMES = [
10
10
  "code_run",
11
11
  "code_action",
12
12
  "code_inspect",
13
- "code_interact",
14
13
  "code_stop",
15
14
  "code_open",
16
15
  "code_export",
17
16
  "code_present",
17
+ "code_check",
18
18
  "code_secret",
19
19
  ] as const;
20
20
  export const CodeRunInput = z
21
21
  .object({
22
- id: id.optional(),
22
+ id: id.optional().describe("Saved script resource to run; an app with an index.html interface opens with code_open instead."),
23
23
  resourceId: id
24
24
  .optional()
25
25
  .describe(
26
- "Optional data context for one-off code. Requires Manage; uses this resource database and shared files/KV without changing its source. Test runs use real shared and personal data; writes and capability effects keep normal permissions and approvals.",
26
+ "Optional data context for one-off code. Requires Manage; uses this resource database and shared files/KV without changing its source. Runs use real shared and personal data; writes and capability effects keep normal permissions and approvals.",
27
27
  ),
28
28
  version: z
29
29
  .number()
@@ -36,14 +36,12 @@ export const CodeRunInput = z
36
36
  .min(1)
37
37
  .max(1024 * 1024)
38
38
  .optional()
39
- .describe("One-off JavaScript/TypeScript entry exporting one function. Use code OR a saved resource id; no title or icon needed."),
39
+ .describe("One-off JavaScript/TypeScript script exporting one function. Use code OR a saved resource id; no title or icon needed."),
40
40
  inputPaths: z
41
41
  .array(z.string().min(1).max(500))
42
42
  .max(64)
43
43
  .default([])
44
- .describe(
45
- "Explicit current-chat input files. Scripts can read them; app test runs expose them only through the simulated picker. User apps never receive chat files.",
46
- ),
44
+ .describe("Explicit current-chat input files; the script reads them from its context files. Apps never receive chat files."),
47
45
  })
48
46
  .strict()
49
47
  .refine((input) => Number(input.id !== undefined) + Number(input.code !== undefined) === 1, "Provide exactly one of id or code")
@@ -91,66 +89,38 @@ export const CodeInspectInput = z
91
89
  .max(30000)
92
90
  .default(0)
93
91
  .describe("Wait up to this duration for background work to finish before returning its real state; never restarts work."),
94
- nodeId: z.string().min(1).max(80).optional(),
95
- offset: z.number().int().min(0).default(0),
96
- limit: z.number().int().min(1).max(100).default(20),
97
92
  })
98
93
  .strict();
99
- const CodeInteraction = z
94
+ export const CodeStopInput = z.object({ runId }).strict();
95
+ export const CodeOpenInput = z.object({ id }).strict();
96
+ const AppFilePath = z
97
+ .string()
98
+ .min(1)
99
+ .max(180)
100
+ .regex(/^[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*$/)
101
+ .refine((path) => path.split("/").every((part) => part !== "." && part !== ".."), "Expected a relative path without . or ..");
102
+ export const CodePresentInput = z
100
103
  .object({
101
- id: z.string().min(1).max(80).describe("Control ID or pending modal ID returned by the snapshot."),
102
- event: z
103
- .discriminatedUnion("type", [
104
- z
105
- .object({
106
- type: z.literal("change"),
107
- value: z.union([
108
- z.string(),
109
- z.number().finite(),
110
- z.boolean(),
111
- z.null(),
112
- z.array(z.string()),
113
- z.object({ start: z.iso.date().nullable(), end: z.iso.date().nullable() }).strict(),
114
- ]),
115
- })
116
- .strict(),
117
- z.object({ type: z.literal("select"), key: z.string().nullable() }).strict(),
118
- z.object({ type: z.literal("view"), value: z.enum(["chart", "table"]) }).strict(),
119
- z.object({ type: z.literal("refresh") }).strict(),
120
- z.object({ type: z.literal("request"), request: z.record(z.string(), z.json()) }).strict(),
121
- ])
104
+ id: id
122
105
  .optional()
123
- .describe(
124
- "An event object, never a JSON-encoded string. Omit to activate a button. Example: {type:'change',value:['Sued']} or {type:'view',value:'table'}.",
125
- ),
126
- answer: z.json().optional().describe("Only for the pending modal ID: the modal answer, or null to cancel. Do not use for controls."),
127
- })
128
- .strict();
129
- const validInteraction = (input: z.infer<typeof CodeInteraction>) => input.event === undefined || input.answer === undefined;
130
- export const CodeInteractInput = CodeInteraction.partial({ id: true })
131
- .extend({
132
- runId,
133
- // Three normal 15-second callback budgets share the existing 45-second call budget.
134
- steps: z
135
- .array(CodeInteraction.refine(validInteraction, "Supply event OR answer"))
106
+ .describe("Saved app with an index.html interface; the card runs it live with its data. Omit title to use the app title."),
107
+ files: z
108
+ .array(z.object({ path: AppFilePath, content: z.string().max(1024 * 1024) }).strict())
136
109
  .min(1)
137
- .max(3)
110
+ .max(64)
138
111
  .optional()
139
- .describe(
140
- "Up to three sequential interactions in one call. Use steps OR the top-level id/event/answer. Stops on an error, pending modal or background work; returns completedSteps and nextStep.",
141
- ),
112
+ .describe("One-off app for this chat: index.html plus optional style.css, app.js and other modules. It has no saved data."),
113
+ title: z.string().trim().min(1).max(120).optional().describe("Card title; required with files."),
142
114
  })
143
115
  .strict()
144
- .refine(
145
- (input) =>
146
- input.steps
147
- ? input.id === undefined && input.event === undefined && input.answer === undefined
148
- : input.id !== undefined && (input.event === undefined || input.answer === undefined),
149
- "Supply steps OR one id with event/answer.",
150
- );
151
- export const CodeStopInput = z.object({ runId }).strict();
152
- export const CodeOpenInput = z.object({ id }).strict();
153
- export const CodePresentInput = z.object({ runId, title: z.string().trim().min(1).max(120) }).strict();
116
+ .refine((input) => Number(input.id !== undefined) + Number(input.files !== undefined) === 1, "Provide exactly one of id or files")
117
+ .refine((input) => input.id !== undefined || input.title !== undefined, "A one-off app needs a title")
118
+ .refine((input) => !input.files || input.files.some((file) => file.path === "index.html"), "files must contain index.html");
119
+ export const CodeCheckInput = z
120
+ .object({ id: id.optional(), files: CodePresentInput.shape.files })
121
+ .strict()
122
+ .refine((input) => Number(input.id !== undefined) + Number(input.files !== undefined) === 1, "Provide exactly one of id or files")
123
+ .refine((input) => !input.files || input.files.some((file) => file.path === "index.html"), "files must contain index.html");
154
124
  export const CodeExportInput = z
155
125
  .object({ runId, name: z.string().min(1).max(180).describe("Captured output filename returned by the snapshot.") })
156
126
  .strict();
@@ -179,11 +149,11 @@ export const CodeRuntimeInput = z.discriminatedUnion("operation", [
179
149
  CodeRunInput.safeExtend({ operation: z.literal("run") }),
180
150
  CodeActionInput.safeExtend({ operation: z.literal("action") }),
181
151
  CodeInspectInput.extend({ operation: z.literal("inspect") }),
182
- CodeInteractInput.safeExtend({ operation: z.literal("interact") }),
183
152
  CodeStopInput.extend({ operation: z.literal("stop") }),
184
153
  CodeOpenInput.extend({ operation: z.literal("open") }),
185
154
  CodeSecretInput.extend({ operation: z.literal("secret") }),
186
- CodePresentInput.extend({ operation: z.literal("present") }),
155
+ CodePresentInput.safeExtend({ operation: z.literal("present") }),
156
+ CodeCheckInput.safeExtend({ operation: z.literal("check") }),
187
157
  CodeExportInput.extend({ operation: z.literal("export") }),
188
158
  ]);
189
159
  export type CodeRuntimeInput = z.infer<typeof CodeRuntimeInput>;
@@ -197,12 +167,12 @@ export function parseCodeToolInput(name: string, args: unknown): CodeRuntimeInpu
197
167
  return { operation: "run", ...CodeRunInput.parse(args) };
198
168
  case "code_inspect":
199
169
  return { operation: "inspect", ...CodeInspectInput.parse(args) };
200
- case "code_interact":
201
- return { operation: "interact", ...CodeInteractInput.parse(args) };
202
170
  case "code_stop":
203
171
  return { operation: "stop", ...CodeStopInput.parse(args) };
204
172
  case "code_open":
205
173
  return { operation: "open", ...CodeOpenInput.parse(args) };
174
+ case "code_check":
175
+ return { operation: "check", ...CodeCheckInput.parse(args) };
206
176
  case "code_present":
207
177
  return { operation: "present", ...CodePresentInput.parse(args) };
208
178
  case "code_export":
package/src/ai/browser.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  CODE_RUNTIME_TOOL_NAMES,
3
3
  CodeActionInput,
4
+ CodeCheckInput,
4
5
  CodeResourceId,
5
6
  CodeRuntimeInput,
6
7
  CodeToolFailure,
@@ -283,7 +283,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
283
283
  <div class="mt-4 flex flex-col gap-4 text-xs leading-5 text-secondary">
284
284
  <p class="whitespace-pre-wrap">{description()}</p>
285
285
  <Show when={inlineReviewDetails().length > 0}>
286
- <dl class="grid gap-x-5 gap-y-1.5 border-t border-[var(--k2b-border)] pt-3 sm:grid-cols-[max-content_minmax(0,1fr)]">
286
+ <dl class="grid gap-x-5 gap-y-1.5 sm:grid-cols-[max-content_minmax(0,1fr)]">
287
287
  <For each={inlineReviewDetails()}>
288
288
  {(detail) => (
289
289
  <>
@@ -313,7 +313,12 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
313
313
  </pre>
314
314
  }
315
315
  >
316
- <div class="max-h-72 overflow-auto text-xs text-secondary" role="region" tabIndex={0} aria-label={detail.label}>
316
+ <div
317
+ class="-mx-2 max-h-72 overflow-auto px-2 text-xs text-secondary"
318
+ role="region"
319
+ tabIndex={0}
320
+ aria-label={detail.label}
321
+ >
317
322
  <MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
318
323
  </div>
319
324
  </Show>
@@ -323,10 +328,12 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
323
328
  </div>
324
329
  </Show>
325
330
  </div>
326
- <footer
327
- class="flex min-h-12 flex-wrap items-center gap-2 border-t border-[var(--k2b-border)] bg-[var(--k2b-surface-subtle)] px-4 py-2.5"
328
- data-ai-approval-footer
329
- >
331
+ {/* The decision continues the card below its content: spacing, not a line or a band, sets it apart. */}
332
+ <footer class="flex min-h-12 flex-wrap items-center gap-2 px-4 pb-3" data-ai-approval-footer>
333
+ {/* Present from the start, so screen readers announce progress after the decision buttons leave. */}
334
+ <span class="k2b-sr-only" role="status" aria-live="polite">
335
+ {approval.loading() ? t().submitting : submitted() ? t().submitted : ""}
336
+ </span>
330
337
  <Show when={reviewLinks().length > 0}>
331
338
  <nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
332
339
  <For each={reviewLinks()}>
@@ -339,7 +346,9 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
339
346
  </nav>
340
347
  </Show>
341
348
  <Show when={approval.error()}>
342
- <p class="text-xs text-red-700 dark:text-red-300">{t().approvalFailed}</p>
349
+ <p class="text-xs text-red-700 dark:text-red-300" role="alert">
350
+ {t().approvalFailed}
351
+ </p>
343
352
  </Show>
344
353
  <div class="ml-auto shrink-0">
345
354
  <Show
@@ -9,12 +9,15 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
9
9
  /**
10
10
  * The live chat timeline for a browser test, folded from wire events like the
11
11
  * controller does. `window.emit(event)` applies one event,
12
- * `window.steer(text)` appends a pending steer as the controller does, and
13
- * `window.reconnect(on)` sets whether the stream reconnects.
12
+ * `window.steer(text)` appends a pending steer as the controller does,
13
+ * `window.reconnect(on)` sets whether the stream reconnects, and
14
+ * `window.continued` lists the messages that **Continue** sent.
14
15
  */
15
16
  const [state, setState] = createStore(emptyProjection());
16
17
  const [reconnecting, setReconnecting] = createSignal(false);
18
+ const continued: string[] = [];
17
19
  Object.assign(window, {
20
+ continued,
18
21
  reconnect: (on: boolean) => setReconnecting(on),
19
22
  emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
20
23
  steer: (text: string) =>
@@ -29,7 +32,7 @@ root.style.cssText = "display: flex; flex-direction: column; height: 100dvh; max
29
32
  document.body.append(root);
30
33
  render(
31
34
  () => (
32
- <AiChatActionsProvider actions={{}}>
35
+ <AiChatActionsProvider actions={{ onContinueTurn: (message) => void continued.push(message) }}>
33
36
  {(() => {
34
37
  const items = createAiChatTimeline({
35
38
  messages: createMemo(() => visibleMessages(state)),