@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -3
- package/corpus/templates/analytics/changelog/2026-09-24-analytics-sidebar-footer-aligns-consistently.md +6 -0
- package/corpus/templates/analytics/changelog/2026-09-24-dashboard-emails-match-stacked-area-totals.md +6 -0
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +47 -20
- package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-09-24-guest-inputs-wrap-beneath-existing-guests-so-the-active-inpu.md +6 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +8 -3
- package/corpus/templates/clips/app/lib/recording-link.ts +21 -0
- package/corpus/templates/clips/app/routes/record.tsx +27 -8
- package/corpus/templates/clips/changelog/2026-09-24-the-camera-bubble-stays-visible-while-you-reposition-it-befo.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +7 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +63 -55
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +251 -74
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +66 -56
- package/corpus/templates/design/app/components/design/EditPanel.tsx +19 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +59 -9
- package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +4 -0
- package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +119 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +48 -51
- package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +1 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +298 -187
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
- package/corpus/templates/design/app/components/design/inspector/index.ts +4 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -2
- package/corpus/templates/design/app/i18n/en-US.ts +6 -0
- package/corpus/templates/design/app/pages/Present.tsx +101 -87
- package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +75 -14
- package/corpus/templates/design/app/pages/design-editor/commands/README.md +1 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +69 -56
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +16 -0
- package/corpus/templates/design/app/pages/design-editor/commands/scale-selection.ts +17 -2
- package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +23 -30
- package/corpus/templates/design/app/pages/design-editor/commands/swap-fill-stroke.ts +105 -0
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +21 -0
- package/corpus/templates/design/app/pages/present-review-state.ts +7 -1
- package/corpus/templates/design/changelog/2026-09-24-a-vector-s-stroke-row-shows-position-beside-weight-instead-o.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-css-margins-can-be-adjusted-on-the-canvas-and-in-the-layout-.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-design-reviews-show-the-real-canvas-beside-its-conversation.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-double-clicking-text-or-a-vector-on-the-board-starts-editing.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-interact-opens-a-focused-responsive-preview-without-reloadin.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-keep-gradient-values-visible-and-limit-canvas-paints-to-supp.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-open-pen-paths-no-longer-paint-a-filled-triangle-after-shift.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-re-clicking-the-active-responsive-preview-returns-to-the-can.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-resized-vectors-can-be-point-edited-and-the-k-scale-tool-sho.md +6 -0
- package/corpus/templates/design/changelog/2026-09-24-while-editing-text-click-to-place-the-caret-and-double-click.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +19 -2
- package/corpus/templates/mail/app/components/email/ComposeModal.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-09-24-gmail-push-ack.md +6 -0
- package/corpus/templates/mail/changelog/2026-09-24-the-composer-keeps-its-taller-layout-in-a-narrower-window.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +26 -1
- package/corpus/templates/mail/server/lib/inbox-store.ts +68 -23
- package/corpus/templates/mail/server/lib/inbox-sync.ts +43 -17
- package/corpus/templates/mail/server/plugins/db.ts +19 -0
- package/corpus/templates/mail/server/routes/api/gmail/push.post.ts +4 -15
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +45 -30
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +18 -2
- package/corpus/templates/slides/actions/export-html.ts +28 -17
- package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +27 -15
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-09-23-decks-with-no-design-system-now-follow-a-theme-chosen-for-th.md +6 -0
- package/corpus/templates/slides/shared/slide-background.ts +3 -1
- package/dist/agent/engine/builder-engine.js +11 -0
- package/dist/agent/engine/types.d.ts +1 -0
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.js +32 -4
- package/dist/agent/run-loop-with-resume.js +6 -0
- package/dist/cli/index.js +4 -4
- package/dist/cli/process.d.ts +10 -1
- package/dist/cli/process.js +66 -0
- package/dist/cli/workspace-dev.js +2 -0
- package/dist/client/agent-chat.js +13 -14
- package/dist/client/error-format.js +4 -4
- package/dist/client/observability/ObservabilityDashboard.js +95 -65
- package/dist/client/observability/OutputPreview.d.ts +2 -1
- package/dist/client/observability/OutputPreview.js +64 -7
- package/dist/client/observability/useObservability.d.ts +2 -1
- package/dist/client/observability/useObservability.js +5 -1
- package/dist/client/settings/UsageSection.js +105 -17
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +44 -0
- package/dist/db/client.js +62 -3
- package/dist/db/create-get-db.js +23 -1
- package/dist/db/embedded-runtime.d.ts +21 -0
- package/dist/db/embedded-runtime.js +27 -0
- package/dist/db/migrations.js +2 -0
- package/dist/db/server-runtime.d.ts +22 -0
- package/dist/db/server-runtime.js +26 -0
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.js +204 -121
- package/dist/feature-flags/index.d.ts +1 -1
- package/dist/feature-flags/index.js +1 -1
- package/dist/feature-flags/registry.d.ts +1 -0
- package/dist/feature-flags/registry.js +5 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/webhook-handler.js +4 -1
- package/dist/jobs/background-automation-runner.js +6 -1
- package/dist/localization/core-messages/ar-SA.js +9 -1
- package/dist/localization/core-messages/de-DE.js +9 -1
- package/dist/localization/core-messages/en-US.d.ts +9 -1
- package/dist/localization/core-messages/en-US.js +9 -1
- package/dist/localization/core-messages/es-ES.js +9 -1
- package/dist/localization/core-messages/fr-FR.js +9 -1
- package/dist/localization/core-messages/hi-IN.js +9 -1
- package/dist/localization/core-messages/ja-JP.js +9 -1
- package/dist/localization/core-messages/ko-KR.js +9 -1
- package/dist/localization/core-messages/pt-BR.js +9 -1
- package/dist/localization/core-messages/supplemental/ar-SA.js +1 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +1 -0
- package/dist/localization/core-messages/supplemental/en-US.js +1 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +1 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +1 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +1 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +1 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +1 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +1 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +1 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +9 -1
- package/dist/localization/core-messages/zh-TW.js +9 -1
- package/dist/localization/core-messages.d.ts +9 -1
- package/dist/localization/mcp-settings-messages.d.ts +1 -0
- package/dist/mcp/connect-route.js +45 -8
- package/dist/mcp/connect-store.d.ts +7 -5
- package/dist/mcp/connect-store.js +30 -39
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/actions/get-observability-review-detail.d.ts +7 -0
- package/dist/observability/actions/get-observability-review-detail.js +25 -0
- package/dist/observability/reviews.d.ts +9 -1
- package/dist/observability/reviews.js +20 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/observability/types.d.ts +8 -0
- package/dist/org/handlers.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +9 -9
- package/dist/scripts/db/query.js +6 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/agent-teams.js +4 -1
- package/dist/server/auth.js +60 -20
- package/dist/server/better-auth-instance.js +7 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/core-routes-plugin.js +2 -0
- package/dist/server/embedded.js +6 -0
- package/dist/server/framework-request-handler.js +6 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/usage/actions/get-usage-metrics.js +4 -1
- package/dist/usage/metrics-store.d.ts +19 -0
- package/dist/usage/metrics-store.js +106 -14
- package/dist/usage/store.d.ts +7 -2
- package/dist/usage/store.js +25 -4
- package/dist/vite/action-types-plugin.js +4 -0
- package/dist/vite/client.d.ts +6 -1
- package/dist/vite/client.js +40 -1
- 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 (
|
|
296
|
-
previewLabel, className: "size-full overflow-hidden p-2", "data-preview-kind": "design-thumbnail", role: "img", children:
|
|
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
|
-
|
|
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
|
|
37
|
-
if (billing.unit
|
|
38
|
-
|
|
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.
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
}
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -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;
|
package/dist/db/client.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/db/create-get-db.js
CHANGED
|
@@ -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
|