@agent-native/core 0.187.0 → 0.187.1-nightly-20260925003003

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 (158) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -3
  3. package/corpus/templates/analytics/changelog/2026-09-24-analytics-sidebar-footer-aligns-consistently.md +6 -0
  4. package/corpus/templates/analytics/changelog/2026-09-24-dashboard-emails-match-stacked-area-totals.md +6 -0
  5. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +47 -20
  6. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-09-24-guest-inputs-wrap-beneath-existing-guests-so-the-active-inpu.md +6 -0
  8. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +8 -3
  9. package/corpus/templates/clips/app/lib/recording-link.ts +21 -0
  10. package/corpus/templates/clips/app/routes/record.tsx +27 -8
  11. package/corpus/templates/clips/changelog/2026-09-24-the-camera-bubble-stays-visible-while-you-reposition-it-befo.md +6 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +7 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +63 -55
  14. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +251 -74
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +66 -56
  16. package/corpus/templates/design/app/components/design/EditPanel.tsx +19 -0
  17. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +59 -9
  18. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +4 -0
  19. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +119 -0
  20. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +48 -51
  21. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +1 -0
  22. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +298 -187
  23. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
  24. package/corpus/templates/design/app/components/design/inspector/index.ts +4 -0
  25. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -2
  26. package/corpus/templates/design/app/i18n/en-US.ts +6 -0
  27. package/corpus/templates/design/app/pages/Present.tsx +101 -87
  28. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +75 -14
  29. package/corpus/templates/design/app/pages/design-editor/commands/README.md +1 -0
  30. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +69 -56
  31. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +16 -0
  32. package/corpus/templates/design/app/pages/design-editor/commands/scale-selection.ts +17 -2
  33. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +23 -30
  34. package/corpus/templates/design/app/pages/design-editor/commands/swap-fill-stroke.ts +105 -0
  35. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +21 -0
  36. package/corpus/templates/design/app/pages/present-review-state.ts +7 -1
  37. package/corpus/templates/design/changelog/2026-09-24-a-vector-s-stroke-row-shows-position-beside-weight-instead-o.md +6 -0
  38. package/corpus/templates/design/changelog/2026-09-24-css-margins-can-be-adjusted-on-the-canvas-and-in-the-layout-.md +6 -0
  39. package/corpus/templates/design/changelog/2026-09-24-design-reviews-show-the-real-canvas-beside-its-conversation.md +6 -0
  40. package/corpus/templates/design/changelog/2026-09-24-double-clicking-text-or-a-vector-on-the-board-starts-editing.md +6 -0
  41. package/corpus/templates/design/changelog/2026-09-24-interact-opens-a-focused-responsive-preview-without-reloadin.md +6 -0
  42. package/corpus/templates/design/changelog/2026-09-24-keep-gradient-values-visible-and-limit-canvas-paints-to-supp.md +6 -0
  43. package/corpus/templates/design/changelog/2026-09-24-open-pen-paths-no-longer-paint-a-filled-triangle-after-shift.md +6 -0
  44. package/corpus/templates/design/changelog/2026-09-24-re-clicking-the-active-responsive-preview-returns-to-the-can.md +6 -0
  45. package/corpus/templates/design/changelog/2026-09-24-resized-vectors-can-be-point-edited-and-the-k-scale-tool-sho.md +6 -0
  46. package/corpus/templates/design/changelog/2026-09-24-while-editing-text-click-to-place-the-caret-and-double-click.md +6 -0
  47. package/corpus/templates/design/e2e/helpers.ts +19 -2
  48. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +1 -1
  49. package/corpus/templates/mail/changelog/2026-09-24-gmail-push-ack.md +6 -0
  50. package/corpus/templates/mail/changelog/2026-09-24-the-composer-keeps-its-taller-layout-in-a-narrower-window.md +6 -0
  51. package/corpus/templates/mail/server/db/schema.ts +26 -1
  52. package/corpus/templates/mail/server/lib/inbox-store.ts +68 -23
  53. package/corpus/templates/mail/server/lib/inbox-sync.ts +43 -17
  54. package/corpus/templates/mail/server/plugins/db.ts +19 -0
  55. package/corpus/templates/mail/server/routes/api/gmail/push.post.ts +4 -15
  56. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +45 -30
  57. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +18 -2
  58. package/corpus/templates/slides/actions/export-html.ts +28 -17
  59. package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +27 -15
  60. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  61. package/corpus/templates/slides/changelog/2026-09-23-decks-with-no-design-system-now-follow-a-theme-chosen-for-th.md +6 -0
  62. package/corpus/templates/slides/shared/slide-background.ts +3 -1
  63. package/dist/agent/engine/builder-engine.js +11 -0
  64. package/dist/agent/engine/types.d.ts +1 -0
  65. package/dist/agent/production-agent.d.ts +2 -0
  66. package/dist/agent/production-agent.js +32 -4
  67. package/dist/agent/run-loop-with-resume.js +6 -0
  68. package/dist/cli/index.js +4 -4
  69. package/dist/cli/process.d.ts +10 -1
  70. package/dist/cli/process.js +66 -0
  71. package/dist/cli/workspace-dev.js +2 -0
  72. package/dist/client/agent-chat.js +13 -14
  73. package/dist/client/error-format.js +4 -4
  74. package/dist/client/observability/ObservabilityDashboard.js +95 -65
  75. package/dist/client/observability/OutputPreview.d.ts +2 -1
  76. package/dist/client/observability/OutputPreview.js +64 -7
  77. package/dist/client/observability/useObservability.d.ts +2 -1
  78. package/dist/client/observability/useObservability.js +5 -1
  79. package/dist/client/settings/UsageSection.js +105 -17
  80. package/dist/collab/routes.d.ts +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/db/client.d.ts +44 -0
  83. package/dist/db/client.js +62 -3
  84. package/dist/db/create-get-db.js +23 -1
  85. package/dist/db/embedded-runtime.d.ts +21 -0
  86. package/dist/db/embedded-runtime.js +27 -0
  87. package/dist/db/migrations.js +2 -0
  88. package/dist/db/server-runtime.d.ts +22 -0
  89. package/dist/db/server-runtime.js +26 -0
  90. package/dist/deploy/build.d.ts +29 -0
  91. package/dist/deploy/build.js +204 -121
  92. package/dist/feature-flags/index.d.ts +1 -1
  93. package/dist/feature-flags/index.js +1 -1
  94. package/dist/feature-flags/registry.d.ts +1 -0
  95. package/dist/feature-flags/registry.js +5 -0
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrations/webhook-handler.js +4 -1
  100. package/dist/jobs/background-automation-runner.js +6 -1
  101. package/dist/localization/core-messages/ar-SA.js +9 -1
  102. package/dist/localization/core-messages/de-DE.js +9 -1
  103. package/dist/localization/core-messages/en-US.d.ts +9 -1
  104. package/dist/localization/core-messages/en-US.js +9 -1
  105. package/dist/localization/core-messages/es-ES.js +9 -1
  106. package/dist/localization/core-messages/fr-FR.js +9 -1
  107. package/dist/localization/core-messages/hi-IN.js +9 -1
  108. package/dist/localization/core-messages/ja-JP.js +9 -1
  109. package/dist/localization/core-messages/ko-KR.js +9 -1
  110. package/dist/localization/core-messages/pt-BR.js +9 -1
  111. package/dist/localization/core-messages/supplemental/ar-SA.js +1 -0
  112. package/dist/localization/core-messages/supplemental/de-DE.js +1 -0
  113. package/dist/localization/core-messages/supplemental/en-US.js +1 -0
  114. package/dist/localization/core-messages/supplemental/es-ES.js +1 -0
  115. package/dist/localization/core-messages/supplemental/fr-FR.js +1 -0
  116. package/dist/localization/core-messages/supplemental/hi-IN.js +1 -0
  117. package/dist/localization/core-messages/supplemental/ja-JP.js +1 -0
  118. package/dist/localization/core-messages/supplemental/ko-KR.js +1 -0
  119. package/dist/localization/core-messages/supplemental/pt-BR.js +1 -0
  120. package/dist/localization/core-messages/supplemental/zh-CN.js +1 -0
  121. package/dist/localization/core-messages/supplemental/zh-TW.js +1 -0
  122. package/dist/localization/core-messages/zh-CN.js +9 -1
  123. package/dist/localization/core-messages/zh-TW.js +9 -1
  124. package/dist/localization/core-messages.d.ts +9 -1
  125. package/dist/localization/mcp-settings-messages.d.ts +1 -0
  126. package/dist/mcp/connect-route.js +45 -8
  127. package/dist/mcp/connect-store.d.ts +7 -5
  128. package/dist/mcp/connect-store.js +30 -39
  129. package/dist/notifications/routes.d.ts +3 -3
  130. package/dist/observability/actions/get-observability-review-detail.d.ts +7 -0
  131. package/dist/observability/actions/get-observability-review-detail.js +25 -0
  132. package/dist/observability/reviews.d.ts +9 -1
  133. package/dist/observability/reviews.js +20 -0
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/observability/types.d.ts +8 -0
  136. package/dist/org/handlers.d.ts +1 -1
  137. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  138. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  139. package/dist/scripts/db/query.js +6 -1
  140. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  141. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  142. package/dist/server/agent-teams.js +4 -1
  143. package/dist/server/auth.js +60 -20
  144. package/dist/server/better-auth-instance.js +7 -1
  145. package/dist/server/core-routes-plugin.d.ts +1 -1
  146. package/dist/server/core-routes-plugin.js +2 -0
  147. package/dist/server/embedded.js +6 -0
  148. package/dist/server/framework-request-handler.js +6 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/usage/actions/get-usage-metrics.js +4 -1
  151. package/dist/usage/metrics-store.d.ts +19 -0
  152. package/dist/usage/metrics-store.js +106 -14
  153. package/dist/usage/store.d.ts +7 -2
  154. package/dist/usage/store.js +25 -4
  155. package/dist/vite/action-types-plugin.js +4 -0
  156. package/dist/vite/client.d.ts +6 -1
  157. package/dist/vite/client.js +40 -1
  158. package/package.json +4 -3
@@ -5,6 +5,10 @@ const MAX_ANSWER_LENGTH = 20_000;
5
5
  const MAX_COLUMNS = 8;
6
6
  const MAX_ROWS = 24;
7
7
  const MAX_MARKDOWN_LINES = MAX_ROWS * 2 + 2;
8
+ const DESIGN_HOST_ORIGIN = "https://design.agent-native.com";
9
+ const BETA_DESIGN_HOST_ORIGIN = "https://beta.design.agent-native.com";
10
+ const DESIGN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
11
+ const DESIGN_URL_PATTERN = /(?:^|[\s([{<"'=])((?:https?:\/\/[^\s<>"'`]+|\/design\/[A-Za-z0-9_-]+(?:[?#][^\s<>"'`]*)?))/gm;
8
12
  const REBINDING_DNS_SUFFIXES = [
9
13
  "nip.io",
10
14
  "sslip.io",
@@ -79,6 +83,40 @@ function safeImageUrl(value) {
79
83
  }
80
84
  return undefined;
81
85
  }
86
+ function safeDesignPreviewUrl(value, baseOrigin) {
87
+ const candidate = boundedString(value);
88
+ if (!candidate || candidate.startsWith("//"))
89
+ return undefined;
90
+ try {
91
+ const baseUrl = baseOrigin ? new URL(baseOrigin) : undefined;
92
+ const url = new URL(candidate, baseUrl);
93
+ const sameOrigin = baseUrl?.origin === url.origin;
94
+ if (url.username ||
95
+ url.password ||
96
+ (url.origin !== DESIGN_HOST_ORIGIN &&
97
+ url.origin !== BETA_DESIGN_HOST_ORIGIN &&
98
+ !sameOrigin)) {
99
+ return undefined;
100
+ }
101
+ const match = url.pathname.match(/^\/design\/([A-Za-z0-9][A-Za-z0-9_-]{0,127})$/);
102
+ if (!match || !DESIGN_ID_PATTERN.test(match[1]))
103
+ return undefined;
104
+ return new URL(`/present/${match[1]}?reviewEmbed=1`, url.origin).toString();
105
+ // coercion-ok: malformed untrusted design URLs are intentionally rejected.
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ function findDesignPreviewUrl(text, baseOrigin) {
112
+ for (const match of text.matchAll(DESIGN_URL_PATTERN)) {
113
+ const candidate = match[1].replace(/[),.;!?]+$/, "");
114
+ const previewUrl = safeDesignPreviewUrl(candidate, baseOrigin);
115
+ if (previewUrl)
116
+ return previewUrl;
117
+ }
118
+ return undefined;
119
+ }
82
120
  function splitTableRow(line) {
83
121
  return line
84
122
  .trim()
@@ -115,7 +153,7 @@ function parseMarkdownTable(answer) {
115
153
  }
116
154
  return undefined;
117
155
  }
118
- function parseStructuredPreview(value) {
156
+ function parseStructuredPreview(value, baseOrigin) {
119
157
  if (!isRecord(value))
120
158
  return undefined;
121
159
  const type = boundedString(value.type)?.toLowerCase();
@@ -204,6 +242,7 @@ function parseStructuredPreview(value) {
204
242
  }
205
243
  if (type === "design") {
206
244
  const imageUrl = safeImageUrl(value.imageUrl ?? value.image);
245
+ const previewUrl = safeDesignPreviewUrl(value.url ?? value.urlPath ?? value.designUrl, baseOrigin);
207
246
  const tokens = Array.isArray(value.tokens)
208
247
  ? value.tokens.slice(0, MAX_COLUMNS).flatMap((token) => {
209
248
  if (!isRecord(token))
@@ -215,13 +254,15 @@ function parseStructuredPreview(value) {
215
254
  : [];
216
255
  const title = boundedString(value.title);
217
256
  const summary = boundedString(value.summary);
218
- if (imageUrl || tokens.length > 0 || title || summary) {
219
- return { kind: "design", title, summary, imageUrl, tokens };
257
+ if (imageUrl || previewUrl || tokens.length > 0 || title || summary) {
258
+ return { kind: "design", title, summary, imageUrl, previewUrl, tokens };
220
259
  }
221
260
  }
222
261
  return undefined;
223
262
  }
224
- export function parseOutputPreview(answer) {
263
+ export function parseOutputPreview(answer, baseOrigin = typeof window === "undefined"
264
+ ? undefined
265
+ : window.location.origin) {
225
266
  const text = answer.trim();
226
267
  if (!text)
227
268
  return { kind: "text", text: "-" };
@@ -230,7 +271,7 @@ export function parseOutputPreview(answer) {
230
271
  }
231
272
  const jsonText = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
232
273
  try {
233
- const structured = parseStructuredPreview(JSON.parse(jsonText));
274
+ const structured = parseStructuredPreview(JSON.parse(jsonText), baseOrigin);
234
275
  if (structured)
235
276
  return structured;
236
277
  // coercion-ok: non-JSON answers intentionally use the text fallback.
@@ -238,6 +279,14 @@ export function parseOutputPreview(answer) {
238
279
  catch {
239
280
  // Plain text and Markdown outputs are expected and remain the fallback.
240
281
  }
282
+ const previewUrl = findDesignPreviewUrl(text, baseOrigin);
283
+ if (previewUrl) {
284
+ return {
285
+ kind: "design",
286
+ previewUrl,
287
+ tokens: [],
288
+ };
289
+ }
241
290
  const imageMatch = text.match(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/i);
242
291
  const imageUrl = imageMatch ? safeImageUrl(imageMatch[2]) : undefined;
243
292
  if (imageUrl) {
@@ -251,6 +300,13 @@ export function parseOutputPreview(answer) {
251
300
  }
252
301
  export function OutputPreview({ answer, previewLabel, inlineApp, inlineAppTitle, compact = false, maxAppHeight, }) {
253
302
  const preview = parseOutputPreview(answer);
303
+ if (preview.kind === "design" && preview.previewUrl) {
304
+ return (_jsx("div", { "aria-label": preview.title ?? previewLabel, className: compact
305
+ ? "relative size-full overflow-hidden bg-background"
306
+ : "relative aspect-[16/10] w-full max-w-full overflow-hidden bg-background", "data-preview-kind": compact ? "design-iframe-thumbnail" : "design-iframe", role: "img", children: _jsx("iframe", { "aria-hidden": "true", className: compact
307
+ ? "pointer-events-none absolute left-0 top-0 h-[600%] w-[600%] origin-top-left scale-[0.166667] border-0"
308
+ : "absolute inset-0 size-full border-0", loading: "lazy", referrerPolicy: "no-referrer", src: preview.previewUrl, tabIndex: -1, title: preview.title ?? previewLabel }) }));
309
+ }
254
310
  if (inlineApp && !compact) {
255
311
  return (_jsxs("div", { className: "min-w-0 space-y-4", children: [answer.trim() && preview.kind === "text" && (_jsx(OutputPreview, { answer: answer, previewLabel: previewLabel })), _jsx(McpAppRenderer, { app: inlineApp, readOnly: true, className: "min-w-0", maxHeight: maxAppHeight })] }));
256
312
  }
@@ -292,8 +348,9 @@ export function OutputPreview({ answer, previewLabel, inlineApp, inlineAppTitle,
292
348
  return (_jsx("img", { src: preview.imageUrl, alt: preview.title ?? previewLabel, className: "size-full object-cover", "data-preview-kind": "design-thumbnail", loading: "lazy", referrerPolicy: "no-referrer" }));
293
349
  }
294
350
  if (compact) {
295
- return (_jsxs("div", { "aria-label": [preview.title, preview.summary].filter(Boolean).join(": ") ||
296
- previewLabel, className: "size-full overflow-hidden p-2", "data-preview-kind": "design-thumbnail", role: "img", children: [_jsx("span", { className: "block h-1 w-1/3 rounded-full bg-primary/80" }), _jsx("span", { className: "mt-2 block h-2 w-3/4 rounded-full bg-foreground/20" }), _jsx("span", { className: "mt-1.5 block h-1.5 w-1/2 rounded-full bg-muted-foreground/20" }), _jsx("span", { className: "mt-2 flex gap-1", children: preview.tokens.slice(0, 4).map((token) => (_jsx("span", { className: "size-3 rounded-full bg-primary/40" }, `${token.label}-${token.value}`))) })] }));
351
+ return (_jsx("div", { "aria-label": [preview.title, preview.summary].filter(Boolean).join(": ") ||
352
+ previewLabel, className: "size-full overflow-hidden p-2", "data-preview-kind": "design-thumbnail", role: "img", children: _jsx("span", { className: "line-clamp-3 block text-left text-[10px] leading-3 text-muted-foreground", children: [preview.title, preview.summary].filter(Boolean).join(" — ") ||
353
+ previewLabel }) }));
297
354
  }
298
355
  return (_jsxs("div", { className: contentClassName, "data-preview-kind": "design", children: [preview.imageUrl && (_jsx("img", { src: preview.imageUrl, alt: preview.title ?? previewLabel, className: "mb-3 max-h-72 w-full rounded object-cover", loading: "lazy", referrerPolicy: "no-referrer" })), preview.title && _jsx("div", { className: "font-medium", children: preview.title }), preview.summary && (_jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: preview.summary })), preview.tokens.length > 0 && (_jsx("dl", { className: "mt-3 divide-y divide-border/70", children: preview.tokens.map((token) => (_jsxs("div", { className: "flex flex-wrap justify-between gap-x-4 gap-y-1 py-2 first:pt-0", children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: token.label }), _jsx("dd", { className: "text-xs", children: token.value })] }, `${token.label}-${token.value}`))) }))] }));
299
356
  }
@@ -1,5 +1,5 @@
1
1
  import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
2
- import type { InstructionUpdate, OutputReviewListRow } from "../../observability/types.js";
2
+ import type { InstructionUpdate, OutputReviewDetail, OutputReviewListRow } from "../../observability/types.js";
3
3
  export interface ObservabilityOverview {
4
4
  totalRuns: number;
5
5
  totalCostCents: number;
@@ -28,6 +28,7 @@ export interface TraceSummary {
28
28
  export declare function useTraces(sinceDays?: number, limit?: number): import("@tanstack/react-query").UseQueryResult<NoInfer<TraceSummary[]>, Error>;
29
29
  export declare function useOutputReviews(sinceDays?: number, limit?: number): import("@tanstack/react-query").UseQueryResult<NoInfer<OutputReviewListRow[]>, Error>;
30
30
  export declare function useOutputReviewApp(runId: string | null): import("@tanstack/react-query").UseQueryResult<NoInfer<AgentMcpAppPayload | null>, Error>;
31
+ export declare function useOutputReviewDetail(runId: string | null): import("@tanstack/react-query").UseQueryResult<NoInfer<OutputReviewDetail>, Error>;
31
32
  export declare function useSaveInstructionUpdate(): import("@tanstack/react-query").UseMutationResult<InstructionUpdate, Error, {
32
33
  runId: string;
33
34
  threadId?: string | null;
@@ -31,11 +31,15 @@ export function useOutputReviews(sinceDays = 7, limit = 100) {
31
31
  sinceMs: Date.now() - sinceDays * 86_400_000,
32
32
  limit,
33
33
  }), [sinceDays, limit]);
34
- return useActionQuery("list-observability-reviews", params, { refetchInterval: 30_000 });
34
+ const query = useActionQuery("list-observability-reviews", params, { refetchInterval: 30_000 });
35
+ return query;
35
36
  }
36
37
  export function useOutputReviewApp(runId) {
37
38
  return useActionQuery("get-observability-review-app", { runId: runId ?? "" }, { enabled: runId !== null, gcTime: 0 });
38
39
  }
40
+ export function useOutputReviewDetail(runId) {
41
+ return useActionQuery("get-observability-review-detail", { runId: runId ?? "" }, { enabled: runId !== null, gcTime: 0 });
42
+ }
39
43
  export function useSaveInstructionUpdate() {
40
44
  return useActionMutation("save-observability-instruction-update");
41
45
  }
@@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "
8
8
  import { IconArrowUpRight, IconBell, IconChartLine, IconChevronDown, IconLoader2, IconMail, IconPencil, IconPlus, IconRefresh, IconUsers, IconX, } from "@tabler/icons-react";
9
9
  import { useState } from "react";
10
10
  import { Link } from "react-router";
11
+ import { useT } from "../i18n.js";
11
12
  import { useActionMutation, useActionQuery } from "../use-action.js";
12
13
  const LOOKBACKS = [
13
14
  { value: 7, label: "7d" },
@@ -33,13 +34,53 @@ function compactNumber(value) {
33
34
  return `${(value / 1_000).toFixed(1)}K`;
34
35
  return Math.round(value).toLocaleString();
35
36
  }
36
- function formatCost(cents, billing) {
37
- if (billing.unit === "builder-credits") {
38
- const credits = (cents / 100) *
37
+ function usageAmount(cents, billing, builderCredits, estimatedBuilderCredits) {
38
+ if (billing.unit !== "usd") {
39
+ if (typeof builderCredits === "number" ||
40
+ typeof estimatedBuilderCredits === "number") {
41
+ return (builderCredits ?? 0) + (estimatedBuilderCredits ?? 0);
42
+ }
43
+ if (billing.unit === "mixed")
44
+ return 0;
45
+ return ((cents / 100) *
39
46
  (billing.hardCostMarginMultiplier ?? 1.25) *
40
- (billing.creditsPerUsd ?? 20);
41
- return `${credits.toLocaleString(undefined, { maximumFractionDigits: 1 })} credits`;
47
+ (billing.creditsPerUsd ?? 20));
42
48
  }
49
+ return cents;
50
+ }
51
+ function formatCost(t, cents, billing, builderCredits, estimatedBuilderCredits, otherCostCents, includeOtherUsd = true) {
52
+ if (billing.unit !== "usd") {
53
+ const parts = [];
54
+ const actual = builderCredits ?? 0;
55
+ const estimated = estimatedBuilderCredits ??
56
+ (typeof builderCredits === "number" || billing.unit === "mixed"
57
+ ? 0
58
+ : usageAmount(cents, billing));
59
+ if (actual > 0) {
60
+ parts.push(`${actual.toLocaleString(undefined, { maximumFractionDigits: 3 })} ${t("agentChat.usage.builderCredits", { defaultValue: "Builder credits" })}`);
61
+ }
62
+ if (estimated > 0) {
63
+ parts.push(t("agentChat.usage.estimatedBuilderCredits", {
64
+ defaultValue: "~{{amount}} estimated credits",
65
+ amount: estimated.toLocaleString(undefined, {
66
+ maximumFractionDigits: 3,
67
+ }),
68
+ }));
69
+ }
70
+ if (includeOtherUsd && otherCostCents && otherCostCents > 0) {
71
+ parts.push(t("agentChat.usage.otherUsdSpend", {
72
+ defaultValue: "{{amount}} other USD",
73
+ amount: formatUsdCost(otherCostCents),
74
+ }));
75
+ }
76
+ return (parts.join(" · ") ||
77
+ t("agentChat.usage.noBuilderCredits", {
78
+ defaultValue: "0 Builder credits",
79
+ }));
80
+ }
81
+ return formatUsdCost(cents);
82
+ }
83
+ function formatUsdCost(cents) {
43
84
  if (cents < 100)
44
85
  return `${cents.toFixed(2)}¢`;
45
86
  return `$${(cents / 100).toLocaleString(undefined, {
@@ -69,11 +110,7 @@ function Trend({ daily, billing, }) {
69
110
  if (daily.length === 0) {
70
111
  return (_jsx("div", { className: "flex h-32 items-center justify-center rounded-lg border border-dashed border-border/70 text-sm text-muted-foreground", children: "No usage recorded in this lookback." }));
71
112
  }
72
- const values = daily.map((day) => billing.unit === "builder-credits"
73
- ? (day.costCents / 100) *
74
- (billing.hardCostMarginMultiplier ?? 1.25) *
75
- (billing.creditsPerUsd ?? 20)
76
- : day.costCents);
113
+ const values = daily.map((day) => usageAmount(day.costCents, billing, day.builderCredits, day.estimatedBuilderCredits));
77
114
  const max = Math.max(...values, 0.01);
78
115
  const points = values.map((value, index) => {
79
116
  const x = daily.length === 1 ? 50 : (index / (daily.length - 1)) * 100;
@@ -90,17 +127,38 @@ function MetricCard({ label, value, detail, accent = false, }) {
90
127
  return (_jsxs("div", { className: "min-w-0 rounded-lg border border-border/70 bg-card px-4 py-4", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsx("div", { className: `mt-1 truncate text-xl font-semibold tracking-[-0.02em] tabular-nums ${accent ? "text-primary" : "text-foreground"}`, children: value }), _jsx("div", { className: "mt-1 truncate text-xs text-muted-foreground", children: detail })] }));
91
128
  }
92
129
  function DriverList({ title, rows, billing, }) {
93
- const max = Math.max(...rows.map((row) => row.costCents), 1);
94
- return (_jsxs("div", { children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsx("span", { className: "text-[11px] uppercase tracking-[0.08em] text-muted-foreground", children: "Spend" })] }), rows.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No usage recorded yet." })) : (_jsx("div", { className: "space-y-3", children: rows.slice(0, 5).map((row) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [_jsx("span", { className: "min-w-0 truncate text-foreground", title: row.label, children: row.label }), _jsx("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: formatCost(row.costCents, billing) })] }), _jsx("div", { className: "mt-1.5 h-1 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary/70", style: {
95
- width: `${Math.max(4, (row.costCents / max) * 100)}%`,
96
- } }) })] }, row.key))) }))] }));
130
+ const t = useT();
131
+ const max = Math.max(...rows.map((row) => usageAmount(row.costCents, billing, row.builderCredits, row.estimatedBuilderCredits)), 1);
132
+ const maxOtherUsd = Math.max(...rows.map((row) => row.otherCostCents ?? 0), 1);
133
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsx("span", { className: "text-[11px] uppercase tracking-[0.08em] text-muted-foreground", children: billing.unit === "mixed"
134
+ ? t("agentChat.usage.driverCreditsAndUsd", {
135
+ defaultValue: "Builder credits / USD",
136
+ })
137
+ : billing.unit === "builder-credits"
138
+ ? t("agentChat.usage.builderCredits", {
139
+ defaultValue: "Builder credits",
140
+ })
141
+ : "Spend" })] }), rows.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No usage recorded yet." })) : (_jsx("div", { className: "space-y-3", children: rows.slice(0, 5).map((row) => {
142
+ const builderAmount = usageAmount(row.costCents, billing, row.builderCredits, row.estimatedBuilderCredits);
143
+ const otherOnly = billing.unit === "mixed" && builderAmount === 0;
144
+ const amount = otherOnly
145
+ ? (row.otherCostCents ?? 0)
146
+ : builderAmount;
147
+ const scale = otherOnly ? maxOtherUsd : max;
148
+ return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [_jsx("span", { className: "min-w-0 truncate text-foreground", title: row.label, children: row.label }), _jsx("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: formatCost(t, row.costCents, billing, row.builderCredits, row.estimatedBuilderCredits, row.otherCostCents) })] }), _jsx("div", { className: "mt-1.5 h-1 overflow-hidden rounded-full bg-muted", children: amount > 0 ? (_jsx("div", { className: "h-full rounded-full bg-primary/70", style: {
149
+ width: `${Math.max(4, (amount / scale) * 100)}%`,
150
+ } })) : null })] }, row.key));
151
+ }) }))] }));
97
152
  }
98
153
  function UsageLoadingState() {
99
154
  return (_jsxs("div", { className: "space-y-5", role: "status", "aria-label": "Loading usage", children: [_jsx("div", { className: "grid gap-3 sm:grid-cols-3", children: ["w-24", "w-16", "w-20"].map((width) => (_jsxs("div", { className: "rounded-lg border border-border/70 p-4", children: [_jsx(Skeleton, { className: `h-3 ${width}` }), _jsx(Skeleton, { className: "mt-3 h-6 w-24" }), _jsx(Skeleton, { className: "mt-2 h-3 w-28" })] }, width))) }), _jsxs("div", { className: "rounded-lg border border-border/70 p-4", children: [_jsx(Skeleton, { className: "h-4 w-32" }), _jsx(Skeleton, { className: "mt-3 h-32 w-full" })] })] }));
100
155
  }
101
156
  function AlertEditor({ draft, onChange, onSave, onCancel, isPending, }) {
157
+ const t = useT();
102
158
  const channelCount = Number(draft.inApp) + Number(draft.email);
103
- return (_jsxs("div", { className: "mt-3 rounded-lg border border-border/70 bg-muted/20 p-3", children: [_jsxs("div", { className: "grid gap-3 sm:grid-cols-[1fr_1fr_1fr]", children: [_jsxs("label", { className: "space-y-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: "Unit" }), _jsxs(Select, { value: draft.unit, onValueChange: (value) => onChange({ unit: value }), children: [_jsx(SelectTrigger, { className: "h-9 bg-background text-sm", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "usd", children: "Dollars" }), _jsx(SelectItem, { value: "builder-credits", children: "Builder credits" }), _jsx(SelectItem, { value: "tokens", children: "Tokens" })] })] })] }), _jsxs("label", { className: "space-y-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: "Threshold" }), _jsx(Input, { type: "number", min: "0.01", step: draft.unit === "tokens" ? "1" : "0.01", value: draft.limit, onChange: (event) => onChange({ limit: event.target.value }), className: "h-9 bg-background text-sm" })] }), _jsxs("label", { className: "space-y-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: "Window" }), _jsxs(Select, { value: draft.period, onValueChange: (value) => onChange({ period: value }), children: [_jsx(SelectTrigger, { className: "h-9 bg-background text-sm", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "day", children: "Per day" }), _jsx(SelectItem, { value: "month", children: "Per month" })] })] })] })] }), _jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-4 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: "Deliver via" }), _jsxs("label", { className: "inline-flex items-center gap-2", children: [_jsx(Checkbox, { checked: draft.inApp, onCheckedChange: (checked) => onChange({ inApp: checked === true }) }), _jsx(IconBell, { className: "size-3.5" }), " In app"] }), _jsxs("label", { className: "inline-flex items-center gap-2", children: [_jsx(Checkbox, { checked: draft.email, onCheckedChange: (checked) => onChange({ email: checked === true }) }), _jsx(IconMail, { className: "size-3.5" }), " Email"] }), _jsxs("div", { className: "ms-auto flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancel, children: "Cancel" }), _jsxs(Button, { type: "button", size: "sm", onClick: onSave, disabled: isPending || channelCount === 0, children: [isPending ? _jsx(IconLoader2, { className: "animate-spin" }) : null, "Save alert"] })] })] })] }));
159
+ return (_jsxs("div", { className: "mt-3 rounded-lg border border-border/70 bg-muted/20 p-3", children: [_jsxs("div", { className: "grid gap-3 sm:grid-cols-[1fr_1fr_1fr]", children: [_jsxs("label", { className: "space-y-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: "Unit" }), _jsxs(Select, { value: draft.unit, onValueChange: (value) => onChange({ unit: value }), children: [_jsx(SelectTrigger, { className: "h-9 bg-background text-sm", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "usd", children: "Dollars" }), _jsx(SelectItem, { value: "builder-credits", children: t("agentChat.usage.builderCredits", {
160
+ defaultValue: "Builder credits",
161
+ }) }), _jsx(SelectItem, { value: "tokens", children: "Tokens" })] })] })] }), _jsxs("label", { className: "space-y-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: "Threshold" }), _jsx(Input, { type: "number", min: "0.01", step: draft.unit === "tokens" ? "1" : "0.01", value: draft.limit, onChange: (event) => onChange({ limit: event.target.value }), className: "h-9 bg-background text-sm" })] }), _jsxs("label", { className: "space-y-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: "Window" }), _jsxs(Select, { value: draft.period, onValueChange: (value) => onChange({ period: value }), children: [_jsx(SelectTrigger, { className: "h-9 bg-background text-sm", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "day", children: "Per day" }), _jsx(SelectItem, { value: "month", children: "Per month" })] })] })] })] }), _jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-4 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: "Deliver via" }), _jsxs("label", { className: "inline-flex items-center gap-2", children: [_jsx(Checkbox, { checked: draft.inApp, onCheckedChange: (checked) => onChange({ inApp: checked === true }) }), _jsx(IconBell, { className: "size-3.5" }), " In app"] }), _jsxs("label", { className: "inline-flex items-center gap-2", children: [_jsx(Checkbox, { checked: draft.email, onCheckedChange: (checked) => onChange({ email: checked === true }) }), _jsx(IconMail, { className: "size-3.5" }), " Email"] }), _jsxs("div", { className: "ms-auto flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancel, children: "Cancel" }), _jsxs(Button, { type: "button", size: "sm", onClick: onSave, disabled: isPending || channelCount === 0, children: [isPending ? _jsx(IconLoader2, { className: "animate-spin" }) : null, "Save alert"] })] })] })] }));
104
162
  }
105
163
  function UsageAlertsSection({ appId }) {
106
164
  const query = useActionQuery("get-usage-alerts", {
@@ -176,6 +234,7 @@ function UsageAlertsSection({ appId }) {
176
234
  }), editing && !editing.ruleId ? (_jsxs("div", { className: "px-4 py-4", children: [_jsx("div", { className: "mb-2 text-xs font-medium text-muted-foreground", children: appLabel(editing.appId) }), _jsx(AlertEditor, { draft: editing, onChange: (patch) => setEditing((current) => current ? { ...current, ...patch } : current), onSave: saveDraft, onCancel: () => setEditing(null), isPending: mutation.isPending })] })) : null] })] }));
177
235
  }
178
236
  export function UsageSection({ appId = null, viewAllHref, }) {
237
+ const t = useT();
179
238
  const [sinceDays, setSinceDays] = useState(30);
180
239
  const [scope, setScope] = useState("me");
181
240
  const [selectedUserEmail, setSelectedUserEmail] = useState(null);
@@ -199,8 +258,37 @@ export function UsageSection({ appId = null, viewAllHref, }) {
199
258
  return (_jsxs("div", { className: "mx-auto w-full max-w-5xl space-y-6", children: [_jsxs("header", { className: "flex flex-col gap-4 border-b border-border/70 pb-5 lg:flex-row lg:items-end lg:justify-between", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium uppercase tracking-[0.1em] text-primary", children: [_jsx(IconChartLine, { className: "size-4" }), " Usage"] }), _jsxs("h1", { className: "mt-2 text-xl font-semibold tracking-[-0.02em] text-foreground", children: [appLabel(appId), " usage"] }), _jsx("p", { className: "mt-1 max-w-xl text-sm leading-6 text-muted-foreground", children: "A focused view of spend, capacity, and the prompts driving this app." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [data?.access.canViewWorkspace ? (_jsxs(Select, { value: scope, onValueChange: (value) => {
200
259
  setScope(value);
201
260
  setSelectedUserEmail(null);
202
- }, children: [_jsx(SelectTrigger, { className: "h-9 w-32 text-xs", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "me", children: "My usage" }), _jsx(SelectItem, { value: "workspace", children: "Workspace" })] })] })) : null, scope === "workspace" && data ? (_jsxs(Select, { value: selectedUserEmail ?? "all", onValueChange: (value) => setSelectedUserEmail(value === "all" ? null : value), children: [_jsx(SelectTrigger, { className: "h-9 w-44 text-xs", children: _jsx(SelectValue, { placeholder: "All users" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All users" }), data.availableUsers.map((user) => (_jsx(SelectItem, { value: user.email, children: user.email }, user.email)))] })] })) : null, _jsx("div", { className: "flex items-center rounded-md border border-border/70 p-0.5", children: LOOKBACKS.map((range) => (_jsx(Button, { type: "button", variant: sinceDays === range.value ? "secondary" : "ghost", size: "sm", className: "h-8 px-2.5 text-xs", onClick: () => setSinceDays(range.value), children: range.label }, range.value))) }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => void query.refetch(), disabled: query.isFetching, "aria-label": "Refresh usage", title: "Refresh usage", children: query.isFetching ? (_jsx(IconLoader2, { className: "animate-spin" })) : (_jsx(IconRefresh, {})) })] })] }), query.isError && !data ? (_jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-4 text-sm", role: "alert", children: [_jsx("div", { className: "font-medium text-destructive", children: "Usage data couldn\u2019t be loaded." }), _jsx(Button, { type: "button", variant: "link", size: "sm", className: "mt-1 h-auto p-0", onClick: () => void query.refetch(), children: "Try again" })] })) : null, !data && query.isLoading ? _jsx(UsageLoadingState, {}) : null, data ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-3 sm:grid-cols-3", children: [_jsx(MetricCard, { label: billing.label, value: formatCost(data.totals.costCents, billing), detail: `${sinceDays} day lookback`, accent: true }), _jsx(MetricCard, { label: "Calls", value: data.totals.calls.toLocaleString(), detail: `${data.currentDay.calls.toLocaleString()} today` }), _jsx(MetricCard, { label: "Tokens", value: compactNumber(totalTokens), detail: `${compactNumber(data.currentDay.tokens)} today` })] }), _jsxs("section", { className: "rounded-lg border border-border/70 bg-card p-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Daily trend" }), _jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [formatCost(data.currentDay.costCents, billing), " used today \u00B7", " ", data.currentDay.calls.toLocaleString(), " calls"] })] }), _jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [scope === "workspace" ? (_jsx(IconUsers, { className: "size-3.5" })) : null, scope === "workspace"
261
+ }, children: [_jsx(SelectTrigger, { className: "h-9 w-32 text-xs", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "me", children: "My usage" }), _jsx(SelectItem, { value: "workspace", children: "Workspace" })] })] })) : null, scope === "workspace" && data ? (_jsxs(Select, { value: selectedUserEmail ?? "all", onValueChange: (value) => setSelectedUserEmail(value === "all" ? null : value), children: [_jsx(SelectTrigger, { className: "h-9 w-44 text-xs", children: _jsx(SelectValue, { placeholder: "All users" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All users" }), data.availableUsers.map((user) => (_jsx(SelectItem, { value: user.email, children: user.email }, user.email)))] })] })) : null, _jsx("div", { className: "flex items-center rounded-md border border-border/70 p-0.5", children: LOOKBACKS.map((range) => (_jsx(Button, { type: "button", variant: sinceDays === range.value ? "secondary" : "ghost", size: "sm", className: "h-8 px-2.5 text-xs", onClick: () => setSinceDays(range.value), children: range.label }, range.value))) }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => void query.refetch(), disabled: query.isFetching, "aria-label": "Refresh usage", title: "Refresh usage", children: query.isFetching ? (_jsx(IconLoader2, { className: "animate-spin" })) : (_jsx(IconRefresh, {})) })] })] }), query.isError && !data ? (_jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-4 text-sm", role: "alert", children: [_jsx("div", { className: "font-medium text-destructive", children: "Usage data couldn\u2019t be loaded." }), _jsx(Button, { type: "button", variant: "link", size: "sm", className: "mt-1 h-auto p-0", onClick: () => void query.refetch(), children: "Try again" })] })) : null, !data && query.isLoading ? _jsx(UsageLoadingState, {}) : null, data ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: `grid gap-3 ${billing.unit === "mixed" ? "sm:grid-cols-2 xl:grid-cols-4" : "sm:grid-cols-3"}`, children: [_jsx(MetricCard, { label: billing.unit === "builder-credits"
262
+ ? t("agentChat.usage.builderCredits", {
263
+ defaultValue: "Builder credits",
264
+ })
265
+ : billing.unit === "mixed"
266
+ ? t("agentChat.usage.builderCredits", {
267
+ defaultValue: "Builder credits",
268
+ })
269
+ : billing.label, value: formatCost(t, data.totals.costCents, billing, data.totals.builderCredits, data.totals.estimatedBuilderCredits, data.totals.otherCostCents, false), detail: `${sinceDays} day lookback`, accent: true }), billing.unit === "mixed" ? (_jsx(MetricCard, { label: t("agentChat.usage.otherUnclassifiedSpend", {
270
+ defaultValue: "Other or unclassified USD spend",
271
+ }), value: formatUsdCost(data.totals.otherCostCents ?? 0), detail: t("agentChat.usage.providerSpendDetail", {
272
+ defaultValue: "Provider or older calls outside Builder billing",
273
+ }) })) : null, _jsx(MetricCard, { label: "Calls", value: data.totals.calls.toLocaleString(), detail: `${data.currentDay.calls.toLocaleString()} today` }), _jsx(MetricCard, { label: "Tokens", value: compactNumber(totalTokens), detail: `${compactNumber(data.currentDay.tokens)} today` })] }), _jsxs("section", { className: "rounded-lg border border-border/70 bg-card p-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Daily trend" }), _jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [formatCost(t, data.currentDay.costCents, billing, data.currentDay.credits, data.currentDay.estimatedBuilderCredits, data.currentDay.otherCostCents, false), " ", "used today \u00B7 ", data.currentDay.calls.toLocaleString(), " calls"] }), billing.unit === "mixed" &&
274
+ (data.currentDay.otherCalls ?? 0) > 0 ? (_jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("agentChat.usage.providerSpendToday", {
275
+ defaultValue: "Other or unclassified usage: {{amount}} today",
276
+ amount: formatUsdCost(data.currentDay.otherCostCents ?? 0),
277
+ }) })) : null] }), _jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [scope === "workspace" ? (_jsx(IconUsers, { className: "size-3.5" })) : null, scope === "workspace"
203
278
  ? (selectedUserEmail ?? "All users")
204
- : "Your usage"] })] }), _jsx("div", { className: "mt-4", children: _jsx(Trend, { daily: data.daily, billing: billing }) })] }), _jsxs("details", { className: "group rounded-lg border border-border/70 bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-4 outline-none transition-colors hover:bg-accent/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset [&::-webkit-details-marker]:hidden", children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-semibold text-foreground", children: "Top usage drivers" }), _jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: "Breakdowns stay collapsed until you need them." })] }), _jsx(IconChevronDown, { className: "size-4 text-muted-foreground transition-transform group-open:rotate-180" })] }), _jsxs("div", { className: "grid gap-6 border-t border-border/70 px-4 py-4 md:grid-cols-2", children: [_jsx(DriverList, { title: "By workflow", rows: data.byLabel, billing: billing }), _jsx(DriverList, { title: "By model", rows: data.byModel, billing: billing })] })] }), _jsxs("details", { className: "group rounded-lg border border-border/70 bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-4 outline-none transition-colors hover:bg-accent/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset [&::-webkit-details-marker]:hidden", children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-semibold text-foreground", children: "Recent prompts" }), _jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: "Latest recorded prompts for the selected app and user scope." })] }), _jsxs("span", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [data.recent.length, _jsx(IconChevronDown, { className: "size-4 transition-transform group-open:rotate-180" })] })] }), _jsx("div", { className: "border-t border-border/70", children: data.recent.length === 0 ? (_jsx("p", { className: "px-4 py-5 text-sm text-muted-foreground", children: "No recent prompts recorded." })) : (_jsx("div", { className: "divide-y divide-border/60", children: data.recent.map((entry) => (_jsxs("div", { className: "px-4 py-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 text-[11px] text-muted-foreground", children: [_jsxs("span", { children: [entry.label, " \u00B7 ", entry.model] }), _jsx("span", { children: entry.ownerEmail })] }), _jsx("p", { className: "mt-1 line-clamp-2 text-sm text-foreground", children: entry.prompt ??
279
+ : "Your usage"] })] }), _jsx("div", { className: "mt-4", children: _jsxs("div", { className: billing.unit === "mixed" &&
280
+ (data.totals.otherCostCents ?? 0) > 0
281
+ ? "grid gap-4 md:grid-cols-2"
282
+ : "", children: [_jsxs("div", { children: [billing.unit === "mixed" ? (_jsx("h3", { className: "mb-2 text-xs font-medium text-muted-foreground", children: t("agentChat.usage.builderCredits", {
283
+ defaultValue: "Builder credits",
284
+ }) })) : null, _jsx(Trend, { daily: data.daily, billing: billing })] }), billing.unit === "mixed" &&
285
+ (data.totals.otherCostCents ?? 0) > 0 ? (_jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-xs font-medium text-muted-foreground", children: t("agentChat.usage.otherUnclassifiedSpend", {
286
+ defaultValue: "Other or unclassified USD spend",
287
+ }) }), _jsx(Trend, { daily: data.daily.map((day) => ({
288
+ date: day.date,
289
+ costCents: day.otherCostCents ?? 0,
290
+ calls: day.otherCalls ?? 0,
291
+ tokens: 0,
292
+ })), billing: { unit: "usd", label: "USD" } })] })) : null] }) })] }), _jsxs("details", { className: "group rounded-lg border border-border/70 bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-4 outline-none transition-colors hover:bg-accent/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset [&::-webkit-details-marker]:hidden", children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-semibold text-foreground", children: "Top usage drivers" }), _jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: "Breakdowns stay collapsed until you need them." })] }), _jsx(IconChevronDown, { className: "size-4 text-muted-foreground transition-transform group-open:rotate-180" })] }), _jsxs("div", { className: "grid gap-6 border-t border-border/70 px-4 py-4 md:grid-cols-2", children: [_jsx(DriverList, { title: "By workflow", rows: data.byLabel, billing: billing }), _jsx(DriverList, { title: "By model", rows: data.byModel, billing: billing })] })] }), _jsxs("details", { className: "group rounded-lg border border-border/70 bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-4 outline-none transition-colors hover:bg-accent/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset [&::-webkit-details-marker]:hidden", children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-semibold text-foreground", children: "Recent prompts" }), _jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: "Latest recorded prompts for the selected app and user scope." })] }), _jsxs("span", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [data.recent.length, _jsx(IconChevronDown, { className: "size-4 transition-transform group-open:rotate-180" })] })] }), _jsx("div", { className: "border-t border-border/70", children: data.recent.length === 0 ? (_jsx("p", { className: "px-4 py-5 text-sm text-muted-foreground", children: "No recent prompts recorded." })) : (_jsx("div", { className: "divide-y divide-border/60", children: data.recent.map((entry) => (_jsxs("div", { className: "px-4 py-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 text-[11px] text-muted-foreground", children: [_jsxs("span", { children: [entry.label, " \u00B7 ", entry.model] }), _jsx("span", { children: entry.ownerEmail })] }), _jsx("p", { className: "mt-1 line-clamp-2 text-sm text-foreground", children: entry.prompt ??
205
293
  "Prompt text was not captured for this call." })] }, entry.id))) })) })] }), _jsx(UsageAlertsSection, { appId: appId }), viewAllHref ? (_jsx("div", { className: "flex justify-end", children: _jsx(Button, { asChild: true, variant: "link", size: "sm", className: "gap-1.5", children: _jsxs(Link, { to: viewAllHref, children: ["View all usage", _jsx(IconArrowUpRight, {})] }) }) })) : null] })) : null] }));
206
294
  }
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -172,6 +172,16 @@ export declare function isProductionServerlessFunctionRuntime(env?: NodeJS.Proce
172
172
  * by the AWS Lambda execution environment itself only once a function actually
173
173
  * invokes; `NETLIFY_FUNCTION_NAME` and the Vercel function markers are the
174
174
  * same kind of invocation-only signal on their platforms.
175
+ *
176
+ * Cloudflare is checked first and outside the `NODE_ENV` gate below:
177
+ * `hasCloudflareRuntime()` (`__cf_env` / `__env__` on `globalThis`) is only
178
+ * ever set by the generated Worker `fetch` handler on an actual request (see
179
+ * `generateCloudflareModuleWorkerEntry()` in deploy/build.ts) — it is never
180
+ * present in the Node process that runs the build, including the Cloudflare
181
+ * Pages static-shell prerender, which spawns a plain Node subprocess with no
182
+ * Workers globals. Unlike Netlify/Lambda/Vercel, Cloudflare's own runtime does
183
+ * not reliably set `NODE_ENV=production`, so gating it on that check the same
184
+ * way the other three are would make this branch silently never fire.
175
185
  */
176
186
  export declare function isHostedFunctionInvocationRuntime(env?: NodeJS.ProcessEnv): boolean;
177
187
  /**
@@ -182,6 +192,40 @@ export declare function isHostedFunctionInvocationRuntime(env?: NodeJS.ProcessEn
182
192
  export declare class HostedRuntimeLocalDatabaseError extends Error {
183
193
  constructor(source: string);
184
194
  }
195
+ /**
196
+ * Shared refusal for every database opener. `getDbExec()` (via `initClient`)
197
+ * and `createGetDb()`'s Drizzle opener resolve the same runtime URL and must
198
+ * refuse the same way — a request that reaches Drizzle first used to skip
199
+ * this check entirely and open PGlite silently.
200
+ *
201
+ * `isMigrationAuthorizedRuntime()` is checked first: release scripts and
202
+ * durable background workers can be a real hosted function invocation (a
203
+ * Netlify background function still gets `NETLIFY_FUNCTION_NAME`) and are
204
+ * still allowed to touch PGlite under `withMigrationRuntime()` — that path is
205
+ * guarded separately by `assertReleaseMigrationTargetsRemoteDatabase()`.
206
+ *
207
+ * A real hosted function invocation (`isHostedFunctionInvocationRuntime()`)
208
+ * always refuses next, before `isEmbeddedRuntimeAuthorized()` is even
209
+ * consulted: Netlify/Vercel/Lambda/Cloudflare are never a legitimate context
210
+ * for "this is an intentionally embedded desktop app," regardless of
211
+ * whether the local database was deliberately configured — an embedded host
212
+ * that ends up actually running as one of those ephemeral, multi-instance
213
+ * platforms hits the exact per-instance-PGlite failure the guard exists to
214
+ * prevent either way. `isEmbeddedRuntimeAuthorized()` only exempts the
215
+ * bare Node/Docker branch below: `createAgentNativeEmbeddedPlugin()` hosts
216
+ * (packaged/desktop installs) deliberately pass a `pglite:` `databaseUrl`,
217
+ * and — unlike the `NODE_ENV=test` integration-suite case
218
+ * `isServerRuntimeStarted()`'s own gate assumed — a real packaged install
219
+ * runs with `NODE_ENV=production`, so that gate alone does not exempt it.
220
+ * `configureAgentNativeEmbeddedEnvironment()` claims this duty whenever the
221
+ * embedding host supplies an explicit `databaseUrl`.
222
+ *
223
+ * `isServerRuntimeStarted()` is additionally gated on `NODE_ENV === "production"`
224
+ * here, unlike `isHostedFunctionInvocationRuntime()`'s Cloudflare branch:
225
+ * `getH3App()`'s bootstrap — where the flag is set — also runs for `pnpm dev`
226
+ * and any `NODE_ENV=test` suite that boots a real H3 app.
227
+ */
228
+ export declare function assertHostedRuntimeDatabase(): void;
185
229
  /** True for statements that change database schema or database privileges. */
186
230
  export declare function isSchemaMutationStatement(statement: DbExecStatement): boolean;
187
231
  /**
package/dist/db/client.js CHANGED
@@ -8,8 +8,10 @@ import path from "path";
8
8
  */
9
9
  import { getAppConfig } from "../app-config/index.js";
10
10
  import { getAsyncLocalStorageCtor } from "../shared/optional-node-builtins.js";
11
+ import { isEmbeddedRuntimeAuthorized } from "./embedded-runtime.js";
11
12
  import { isMigrationAuthorizedRuntime } from "./migration-runtime.js";
12
13
  import { beginDatabaseOperation, recordDatabaseRetry, } from "./request-telemetry.js";
14
+ import { isServerRuntimeStarted } from "./server-runtime.js";
13
15
  const recyclingPostgresPools = new WeakSet();
14
16
  const loggedNeonPools = new WeakSet();
15
17
  const PgliteTransactionStorage = getAsyncLocalStorageCtor();
@@ -906,8 +908,20 @@ export function isProductionServerlessFunctionRuntime(env = process.env) {
906
908
  * by the AWS Lambda execution environment itself only once a function actually
907
909
  * invokes; `NETLIFY_FUNCTION_NAME` and the Vercel function markers are the
908
910
  * same kind of invocation-only signal on their platforms.
911
+ *
912
+ * Cloudflare is checked first and outside the `NODE_ENV` gate below:
913
+ * `hasCloudflareRuntime()` (`__cf_env` / `__env__` on `globalThis`) is only
914
+ * ever set by the generated Worker `fetch` handler on an actual request (see
915
+ * `generateCloudflareModuleWorkerEntry()` in deploy/build.ts) — it is never
916
+ * present in the Node process that runs the build, including the Cloudflare
917
+ * Pages static-shell prerender, which spawns a plain Node subprocess with no
918
+ * Workers globals. Unlike Netlify/Lambda/Vercel, Cloudflare's own runtime does
919
+ * not reliably set `NODE_ENV=production`, so gating it on that check the same
920
+ * way the other three are would make this branch silently never fire.
909
921
  */
910
922
  export function isHostedFunctionInvocationRuntime(env = process.env) {
923
+ if (hasCloudflareRuntime())
924
+ return true;
911
925
  if (env.NODE_ENV !== "production" || env.NETLIFY_LOCAL === "true") {
912
926
  return false;
913
927
  }
@@ -932,6 +946,53 @@ export class HostedRuntimeLocalDatabaseError extends Error {
932
946
  this.name = "HostedRuntimeLocalDatabaseError";
933
947
  }
934
948
  }
949
+ /**
950
+ * Shared refusal for every database opener. `getDbExec()` (via `initClient`)
951
+ * and `createGetDb()`'s Drizzle opener resolve the same runtime URL and must
952
+ * refuse the same way — a request that reaches Drizzle first used to skip
953
+ * this check entirely and open PGlite silently.
954
+ *
955
+ * `isMigrationAuthorizedRuntime()` is checked first: release scripts and
956
+ * durable background workers can be a real hosted function invocation (a
957
+ * Netlify background function still gets `NETLIFY_FUNCTION_NAME`) and are
958
+ * still allowed to touch PGlite under `withMigrationRuntime()` — that path is
959
+ * guarded separately by `assertReleaseMigrationTargetsRemoteDatabase()`.
960
+ *
961
+ * A real hosted function invocation (`isHostedFunctionInvocationRuntime()`)
962
+ * always refuses next, before `isEmbeddedRuntimeAuthorized()` is even
963
+ * consulted: Netlify/Vercel/Lambda/Cloudflare are never a legitimate context
964
+ * for "this is an intentionally embedded desktop app," regardless of
965
+ * whether the local database was deliberately configured — an embedded host
966
+ * that ends up actually running as one of those ephemeral, multi-instance
967
+ * platforms hits the exact per-instance-PGlite failure the guard exists to
968
+ * prevent either way. `isEmbeddedRuntimeAuthorized()` only exempts the
969
+ * bare Node/Docker branch below: `createAgentNativeEmbeddedPlugin()` hosts
970
+ * (packaged/desktop installs) deliberately pass a `pglite:` `databaseUrl`,
971
+ * and — unlike the `NODE_ENV=test` integration-suite case
972
+ * `isServerRuntimeStarted()`'s own gate assumed — a real packaged install
973
+ * runs with `NODE_ENV=production`, so that gate alone does not exempt it.
974
+ * `configureAgentNativeEmbeddedEnvironment()` claims this duty whenever the
975
+ * embedding host supplies an explicit `databaseUrl`.
976
+ *
977
+ * `isServerRuntimeStarted()` is additionally gated on `NODE_ENV === "production"`
978
+ * here, unlike `isHostedFunctionInvocationRuntime()`'s Cloudflare branch:
979
+ * `getH3App()`'s bootstrap — where the flag is set — also runs for `pnpm dev`
980
+ * and any `NODE_ENV=test` suite that boots a real H3 app.
981
+ */
982
+ export function assertHostedRuntimeDatabase() {
983
+ if (isMigrationAuthorizedRuntime())
984
+ return;
985
+ if (!isLocalDatabase())
986
+ return;
987
+ if (isHostedFunctionInvocationRuntime()) {
988
+ throw new HostedRuntimeLocalDatabaseError(getRuntimeDatabaseSource());
989
+ }
990
+ if (isEmbeddedRuntimeAuthorized())
991
+ return;
992
+ if (process.env.NODE_ENV === "production" && isServerRuntimeStarted()) {
993
+ throw new HostedRuntimeLocalDatabaseError(getRuntimeDatabaseSource());
994
+ }
995
+ }
935
996
  const SCHEMA_MUTATION_STATEMENT = /^\s*(?:CREATE|ALTER|DROP|TRUNCATE|COMMENT|REINDEX|GRANT|REVOKE)\b/i;
936
997
  function rawSql(statement) {
937
998
  return typeof statement === "string" ? statement : statement.sql;
@@ -1757,9 +1818,7 @@ function guardSchemaMutations(exec) {
1757
1818
  async function initClient() {
1758
1819
  if (_exec)
1759
1820
  return;
1760
- if (isHostedFunctionInvocationRuntime() && isLocalDatabase()) {
1761
- throw new HostedRuntimeLocalDatabaseError(getRuntimeDatabaseSource());
1762
- }
1821
+ assertHostedRuntimeDatabase();
1763
1822
  const url = getRuntimeDatabaseUrl("pglite:./data/pglite");
1764
1823
  _exec = await createDbExecInternal({ url }, true);
1765
1824
  }
@@ -1,4 +1,4 @@
1
- import { getActivePgliteTransactionClient, getRuntimeDatabaseUrl, isPgliteUrl, isConnectionError, getPgliteClient, loadPgliteDrizzle, pgliteDrizzleClient, pgPoolOptions, neonPoolOptions, guardNeonPool, withDbTimeout, retryOnConnectionError, dbOpTimeoutMs, sharedDbPool, onSharedDbPoolsClosed, onSharedDbPoolReplaced, } from "./client.js";
1
+ import { getActivePgliteTransactionClient, getRuntimeDatabaseUrl, isPgliteUrl, isConnectionError, getPgliteClient, loadPgliteDrizzle, pgliteDrizzleClient, pgPoolOptions, neonPoolOptions, guardNeonPool, withDbTimeout, retryOnConnectionError, dbOpTimeoutMs, sharedDbPool, onSharedDbPoolsClosed, onSharedDbPoolReplaced, assertHostedRuntimeDatabase, } from "./client.js";
2
2
  // Lazy driver loaders — cached promises so dynamic import only runs once.
3
3
  let _pgDrizzle;
4
4
  function getPgDrizzle() {
@@ -294,6 +294,21 @@ export function createGetDb(schema) {
294
294
  function startInit() {
295
295
  if (_dbReady)
296
296
  return _dbReady;
297
+ // getDb() below calls this via `void startInit()`, so a throw here must
298
+ // surface through the rejected `_dbReady` it awaits later, not as a
299
+ // synchronous exception out of this function.
300
+ try {
301
+ assertHostedRuntimeDatabase();
302
+ }
303
+ catch (err) {
304
+ _dbReady = Promise.reject(err);
305
+ // The real consumer attaches its handler later, through the lazy proxy's
306
+ // `then` trap, once the caller awaits the query chain — one or more
307
+ // microtasks from now. Without this, Node flags the promise as an
308
+ // unhandled rejection in the gap before that happens.
309
+ _dbReady.catch(() => { });
310
+ return _dbReady;
311
+ }
297
312
  const url = getRuntimeDatabaseUrl("pglite:./data/pglite");
298
313
  if (isPgliteUrl(url)) {
299
314
  _dbReady = loadPgliteDrizzle().then(async ({ drizzle }) => {
@@ -356,6 +371,13 @@ export function createGetDb(schema) {
356
371
  }
357
372
  return result;
358
373
  });
374
+ // `ready` can already be rejected (e.g. the hosted-runtime database
375
+ // guard) when this trap fires, and `[prop].bind(promise)` only
376
+ // returns a handler for the *caller* to invoke — it does not attach
377
+ // one itself. Without this, `promise` sits rejected and unhandled
378
+ // for the gap until the caller actually calls the bound function,
379
+ // which is enough for Node to report it as an unhandled rejection.
380
+ promise.catch(() => { });
359
381
  return promise[prop].bind(promise);
360
382
  }
361
383
  // drizzle-orm duck-types "is this an SQL entity" by reading these two