+
{filterIcon("tasks")}
Tasks
diff --git a/apps/webapp/app/components/runs/v3/RunTag.tsx b/apps/webapp/app/components/runs/v3/RunTag.tsx
index 3d6c6751ce3..1defae01674 100644
--- a/apps/webapp/app/components/runs/v3/RunTag.tsx
+++ b/apps/webapp/app/components/runs/v3/RunTag.tsx
@@ -1,5 +1,4 @@
import { useCallback, useMemo, useState } from "react";
-import tagLeftPath from "./tag-left.svg";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
@@ -7,6 +6,26 @@ import { ClipboardCheckIcon, ClipboardIcon, XIcon } from "lucide-react";
type Tag = string | { key: string; value: string };
+function TagNotch() {
+ return (
+
+ );
+}
+
export function RunTag({
tag,
to,
@@ -26,7 +45,7 @@ export function RunTag({
if (typeof tagResult === "string") {
return (
<>
-

+
{tag}
@@ -35,7 +54,7 @@ export function RunTag({
} else {
return (
<>
-

+
{tagResult.key}
diff --git a/apps/webapp/app/components/runs/v3/SpanEvents.tsx b/apps/webapp/app/components/runs/v3/SpanEvents.tsx
index c79a147b86f..069246c89b7 100644
--- a/apps/webapp/app/components/runs/v3/SpanEvents.tsx
+++ b/apps/webapp/app/components/runs/v3/SpanEvents.tsx
@@ -85,7 +85,7 @@ export function SpanEventError({
/>
{enhancedException.message && (
-
+
{enhancedException.message}
diff --git a/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx b/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx
index a82d8b9d0c6..e696f202559 100644
--- a/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx
+++ b/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx
@@ -136,7 +136,9 @@ export function renderPart(part: UIMessage["parts"][number], i: number) {
return (
- {p.text ?? ""}
+
+ {p.text ?? ""}
+
);
diff --git a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
index a5115897f97..ae46cbe867c 100644
--- a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
+++ b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
@@ -483,7 +483,7 @@ function SubAgentContent({ parts }: { parts: any[] }) {
if (partType === "reasoning" && part.text) {
return (
-
diff --git a/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
index 11ef7aa8a52..775efd7c308 100644
--- a/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
+++ b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
@@ -128,7 +128,7 @@ function MetricRow({
bold?: boolean;
}) {
return (
-
+
{label}
+
{label}
{value}
diff --git a/apps/webapp/app/hooks/useThemeColor.ts b/apps/webapp/app/hooks/useThemeColor.ts
index d78fd39fa79..f088d16b525 100644
--- a/apps/webapp/app/hooks/useThemeColor.ts
+++ b/apps/webapp/app/hooks/useThemeColor.ts
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
/**
* Normalize any CSS color (hex, oklch, hsl, ...) to rgb()/rgba() by rendering
@@ -17,16 +17,28 @@ function toRgb(color: string): string {
}
/**
- * Resolve a theme CSS variable to a concrete, animatable color once on mount.
+ * Resolve a theme CSS variable to a concrete, animatable color.
* framer-motion can't interpolate `var()` strings or oklch values, so animated
- * colors must be resolved and normalized first. The fallback is used during
- * SSR and should match the default dark theme (see tailwind.css).
+ * colors must be resolved and normalized first. Resolution happens in an
+ * effect so server and hydration renders both use the fallback — resolving
+ * during render caused hydration style mismatches. Long-lived components
+ * (e.g. the side menu) outlive theme switches, so re-resolve whenever
+ * `data-theme` flips on .
*/
export function useThemeColor(variable: `--${string}`, fallback: string): string {
- const [color] = useState(() => {
- if (typeof document === "undefined") return fallback;
- const value = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
- return value ? toRgb(value) : fallback;
- });
+ const [color, setColor] = useState(fallback);
+ useEffect(() => {
+ const resolve = () => {
+ const value = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
+ if (value) setColor(toRgb(value));
+ };
+ resolve();
+ const observer = new MutationObserver(resolve);
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+ return () => observer.disconnect();
+ }, [variable]);
return color;
}
diff --git a/apps/webapp/app/presenters/v3/UsagePresenter.server.ts b/apps/webapp/app/presenters/v3/UsagePresenter.server.ts
index fe5ab1ec3ce..a3b96cf6ac3 100644
--- a/apps/webapp/app/presenters/v3/UsagePresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/UsagePresenter.server.ts
@@ -1,5 +1,8 @@
import type { DataPoint } from "regression";
-import { linear } from "regression";
+// Default-import: regression is CJS and its named exports aren't statically
+// analyzable under ESM interop.
+import regression from "regression";
+const { linear } = regression;
import type { PrismaClientOrTransaction } from "~/db.server";
import { env } from "~/env.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx
index c84da200ad4..e9d5c7cbafa 100644
--- a/apps/webapp/app/root.tsx
+++ b/apps/webapp/app/root.tsx
@@ -1,11 +1,14 @@
import type { LinksFunction, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
-import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
+import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
import { ExternalScripts } from "remix-utils/external-scripts";
import type { ToastMessage } from "~/models/message.server";
import { commitSession, getSession } from "~/models/message.server";
-import tailwindStylesheetUrl from "~/tailwind.css";
+// Fonts imported here so Vite rebases the urls and emits the woff2 assets
+import "non.geist";
+import "non.geist/mono";
+import tailwindStylesheetUrl from "~/tailwind.css?url";
import { RouteErrorDisplay } from "./components/ErrorDisplay";
import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout";
import { ShortcutsProvider } from "./components/primitives/ShortcutsProvider";
@@ -15,6 +18,7 @@ import { env } from "./env.server";
import { featuresForRequest } from "./features.server";
import { usePostHog } from "./hooks/usePostHog";
import { getUser } from "./services/session.server";
+import { flag } from "~/v3/featureFlags.server";
import { getTimezonePreference } from "./services/preferences/uiPreferences.server";
import { appEnvTitleTag } from "./utils";
@@ -60,6 +64,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};
const user = await getUser(request);
+ // Theme switching is feature-flagged; while off, everyone stays on dark
+ // even if a preference was saved earlier.
+ const showThemeSwitcher = user
+ ? await flag({ key: "hasThemeSwitcher", defaultValue: true })
+ : false;
const headers = new Headers();
headers.append("Set-Cookie", await commitSession(session));
@@ -77,6 +86,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
triggerCliTag: env.TRIGGER_CLI_TAG,
kapa,
timezone,
+ showThemeSwitcher,
+ // Consumed by ResizablePanel: the browser check must match between SSR
+ // and hydration, so it is derived from the request user-agent.
+ isFirefox: /firefox/i.test(request.headers.get("user-agent") ?? ""),
},
{ headers }
);
@@ -118,12 +131,19 @@ export function ErrorBoundary() {
}
export default function App() {
- const { posthogProjectKey, posthogUiHost, kapa: _kapa } = useTypedLoaderData();
+ const {
+ posthogProjectKey,
+ posthogUiHost,
+ kapa: _kapa,
+ user,
+ showThemeSwitcher,
+ } = useTypedLoaderData();
usePostHog(posthogProjectKey, posthogUiHost);
+ const theme = (showThemeSwitcher ? user?.dashboardPreferences.theme : "dark") ?? "dark";
return (
<>
-
+
@@ -137,7 +157,6 @@ export default function App() {
-
>
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx
index 563155468f4..f121900e901 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx
@@ -471,7 +471,7 @@ export default function Page() {
cancelButton={
Cancel
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
index 839b66f70b3..029efaa4d54 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
@@ -190,7 +190,7 @@ export default function Page() {
)}
-
+
How to set these environment variables
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
index e27b11b7545..37d7c58bc5e 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
@@ -11,7 +11,6 @@ import { Feedback } from "~/components/Feedback";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { EnvironmentSelector } from "~/components/navigation/EnvironmentSelector";
import { AnimatedNumber } from "~/components/primitives/AnimatedNumber";
-import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header2 } from "~/components/primitives/Headers";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
@@ -445,21 +444,24 @@ function RateLimitTypeBadge({
switch (rateLimitType) {
case "tokenBucket":
return (
-
- Token bucket
-
+
);
case "fixedWindow":
return (
-
- Fixed window
-
+
);
case "slidingWindow":
return (
-
- Sliding window
-
+
);
default:
return null;
@@ -858,19 +860,32 @@ function getUsageColorClass(
}
}
+function RateLimitTypePill({ className, label }: { className: string; label: string }) {
+ return (
+
+ {label}
+
+ );
+}
+
function SourceBadge({ source }: { source: "default" | "plan" | "override" }) {
const variants: Record
= {
default: {
label: "Default",
- className: "bg-indigo-500/20 text-indigo-400",
+ className: "bg-indigo-500/20 text-indigo-600 dark:text-indigo-400",
},
plan: {
label: "Plan",
- className: "bg-purple-500/20 text-purple-400",
+ className: "bg-purple-500/20 text-purple-600 dark:text-purple-400",
},
override: {
label: "Override",
- className: "bg-amber-500/20 text-amber-400",
+ className: "bg-amber-500/20 text-amber-700 dark:text-amber-400",
},
};
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx
index 783b60cb1ca..136cc06658c 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx
@@ -282,6 +282,7 @@ export default function Page() {
Suggest a new region
(
setFaviconError(false)}
/>
) : (
-
+
)}
{
return json && typeof json === "object" ? (json as Record) : {};
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx
index e58afdb65df..1e92cb08eeb 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx
@@ -668,7 +668,7 @@ function DomainList({ domains }: { domains: ReadonlyArray }) {
{d.domain}
{d.state === "failed" && d.verificationFailedReason && (
-
+
Reason: {d.verificationFailedReason}
)}
diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx
index b4b92c8a133..03bb6ec9147 100644
--- a/apps/webapp/app/routes/account._index/route.tsx
+++ b/apps/webapp/app/routes/account._index/route.tsx
@@ -1,7 +1,14 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { conformZodMessage, parseWithZod } from "@conform-to/zod";
-import { Form, type MetaFunction, useActionData } from "@remix-run/react";
-import { type ActionFunction, json } from "@remix-run/server-runtime";
+import { MoonIcon, SunIcon } from "@heroicons/react/20/solid";
+import {
+ Form,
+ type MetaFunction,
+ useActionData,
+ useFetcher,
+ useLoaderData,
+} from "@remix-run/react";
+import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { AvatarCircleIcon } from "~/assets/icons/AvatarCircleIcon";
import { EnvelopeIcon } from "~/assets/icons/EnvelopeIcon";
@@ -13,6 +20,7 @@ import {
} from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
+import { Select, SelectItem } from "~/components/primitives/Select";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
@@ -26,7 +34,9 @@ import { prisma } from "~/db.server";
import { useUser } from "~/hooks/useUser";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { updateUser } from "~/models/user.server";
-import { requireUserId } from "~/services/session.server";
+import { updateThemePreference } from "~/services/dashboardPreferences.server";
+import { flag } from "~/v3/featureFlags.server";
+import { requireUser, requireUserId } from "~/services/session.server";
import { accountPath } from "~/utils/pathBuilder";
export const meta: MetaFunction = () => {
@@ -75,11 +85,28 @@ function createSchema(
});
}
+export async function loader({ request }: LoaderFunctionArgs) {
+ await requireUserId(request);
+ const showThemeSwitcher = await flag({ key: "hasThemeSwitcher", defaultValue: true });
+ return json({ showThemeSwitcher });
+}
+
export const action: ActionFunction = async ({ request }) => {
const userId = await requireUserId(request);
const formData = await request.formData();
+ if (formData.get("action") === "update-theme") {
+ const showThemeSwitcher = await flag({ key: "hasThemeSwitcher", defaultValue: true });
+ if (!showThemeSwitcher) {
+ return json({ error: "Not available" }, { status: 404 });
+ }
+ const user = await requireUser(request);
+ const theme = formData.get("theme") === "light" ? "light" : "dark";
+ await updateThemePreference({ user, theme });
+ return json({ success: true });
+ }
+
const formSchema = createSchema({
isEmailUnique: async (email) => {
const existingUser = await prisma.user.findFirst({
@@ -126,7 +153,14 @@ export const action: ActionFunction = async ({ request }) => {
export default function Page() {
const user = useUser();
+ const { showThemeSwitcher } = useLoaderData();
const lastSubmission = useActionData();
+ const themeFetcher = useFetcher();
+ const pendingTheme = themeFetcher.formData?.get("theme");
+ const theme =
+ typeof pendingTheme === "string"
+ ? (pendingTheme as "dark" | "light")
+ : (user.dashboardPreferences.theme ?? "dark");
const [form, { name, email, marketingEmails }] = useForm({
id: "account",
@@ -195,6 +229,61 @@ export default function Page() {
/>
+ {showThemeSwitcher && (
+ <>
+
+ Appearance
+
+
+
+
+
+ >
+ )}
diff --git a/apps/webapp/app/routes/api.v1.errors.$errorId.ignore.ts b/apps/webapp/app/routes/api.v1.errors.$errorId.ignore.ts
index 4b0ad2ab671..9aba43042c9 100644
--- a/apps/webapp/app/routes/api.v1.errors.$errorId.ignore.ts
+++ b/apps/webapp/app/routes/api.v1.errors.$errorId.ignore.ts
@@ -9,7 +9,7 @@ const ParamsSchema = z.object({
errorId: z.string(),
});
-export const { action, loader } = createActionApiRoute(
+const route = createActionApiRoute(
{
params: ParamsSchema,
body: IgnoreErrorRequestBody,
@@ -56,3 +56,6 @@ export const { action, loader } = createActionApiRoute(
return json(updated);
}
);
+
+export const action = route.action;
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.errors.$errorId.resolve.ts b/apps/webapp/app/routes/api.v1.errors.$errorId.resolve.ts
index e0818c89f49..68d4a094ab8 100644
--- a/apps/webapp/app/routes/api.v1.errors.$errorId.resolve.ts
+++ b/apps/webapp/app/routes/api.v1.errors.$errorId.resolve.ts
@@ -9,7 +9,7 @@ const ParamsSchema = z.object({
errorId: z.string(),
});
-export const { action, loader } = createActionApiRoute(
+const route = createActionApiRoute(
{
params: ParamsSchema,
body: ResolveErrorRequestBody,
@@ -48,3 +48,6 @@ export const { action, loader } = createActionApiRoute(
return json(updated);
}
);
+
+export const action = route.action;
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.errors.$errorId.unresolve.ts b/apps/webapp/app/routes/api.v1.errors.$errorId.unresolve.ts
index 9362b7c4c4b..ba16118c816 100644
--- a/apps/webapp/app/routes/api.v1.errors.$errorId.unresolve.ts
+++ b/apps/webapp/app/routes/api.v1.errors.$errorId.unresolve.ts
@@ -8,7 +8,7 @@ const ParamsSchema = z.object({
errorId: z.string(),
});
-export const { action, loader } = createActionApiRoute(
+const route = createActionApiRoute(
{
params: ParamsSchema,
method: "POST",
@@ -40,3 +40,6 @@ export const { action, loader } = createActionApiRoute(
return json(updated);
}
);
+
+export const action = route.action;
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts b/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts
index feec6dd6554..2943c21cca6 100644
--- a/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts
+++ b/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts
@@ -13,7 +13,7 @@ const BodySchema = z.object({
taskIdentifier: z.string().min(1, "Task identifier is required"),
});
-export const { action } = createActionApiRoute(
+const route = createActionApiRoute(
{
params: ParamsSchema,
body: BodySchema,
@@ -50,3 +50,7 @@ export const { action } = createActionApiRoute(
}
}
);
+
+export const action = route.action;
+// The builder's loader handles CORS OPTIONS preflight
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts
index 33b68ad244a..6b91205e464 100644
--- a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts
+++ b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts
@@ -7,7 +7,8 @@ import { prisma } from "~/db.server";
import { createProject } from "~/models/project.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
-import { isCuid } from "cuid";
+import cuid from "cuid";
+const { isCuid } = cuid;
const ParamsSchema = z.object({
orgParam: z.string(),
diff --git a/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.override.ts b/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.override.ts
index 3223a2a6062..e5e4a926ee6 100644
--- a/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.override.ts
+++ b/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.override.ts
@@ -10,7 +10,7 @@ const BodySchema = z.object({
concurrencyLimit: z.number().int().min(0).max(100000),
});
-export const { action } = createActionApiRoute(
+const route = createActionApiRoute(
{
body: BodySchema,
params: z.object({
@@ -73,3 +73,7 @@ export const { action } = createActionApiRoute(
);
}
);
+
+export const action = route.action;
+// The builder's loader handles CORS OPTIONS preflight
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.reset.ts b/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.reset.ts
index dbeea591ade..a7aca141932 100644
--- a/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.reset.ts
+++ b/apps/webapp/app/routes/api.v1.queues.$queueParam.concurrency.reset.ts
@@ -9,7 +9,7 @@ const BodySchema = z.object({
type: RetrieveQueueType.default("id"),
});
-export const { action } = createActionApiRoute(
+const route = createActionApiRoute(
{
body: BodySchema,
params: z.object({
@@ -73,3 +73,7 @@ export const { action } = createActionApiRoute(
);
}
);
+
+export const action = route.action;
+// The builder's loader handles CORS OPTIONS preflight
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.queues.$queueParam.pause.ts b/apps/webapp/app/routes/api.v1.queues.$queueParam.pause.ts
index 452bd81746b..00f94853f43 100644
--- a/apps/webapp/app/routes/api.v1.queues.$queueParam.pause.ts
+++ b/apps/webapp/app/routes/api.v1.queues.$queueParam.pause.ts
@@ -9,7 +9,7 @@ const BodySchema = z.object({
action: z.enum(["pause", "resume"]),
});
-export const { action } = createActionApiRoute(
+const route = createActionApiRoute(
{
body: BodySchema,
params: z.object({
@@ -44,3 +44,7 @@ export const { action } = createActionApiRoute(
return json(q);
}
);
+
+export const action = route.action;
+// The builder's loader handles CORS OPTIONS preflight
+export const loader = route.loader;
diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts b/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts
index 58cf572f44d..6ad19065045 100644
--- a/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts
+++ b/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts
@@ -1,21 +1,18 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { tryCatch } from "@trigger.dev/core/utils";
-import type { RunMetadataChangeOperation } from "@trigger.dev/core/v3/schemas";
import { UpdateMetadataRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { $replica } from "~/db.server";
-// Aliased to avoid shadowing the local `env: AuthenticatedEnvironment`
-// parameter the route handler and `routeOperationsToRun` use.
+// Aliased to avoid shadowing the local `env` parameter in the handler.
import { env as appEnv } from "~/env.server";
-import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
-import { logger } from "~/services/logger.server";
import { updateMetadataService } from "~/services/metadata/updateMetadataInstance.server";
import { publishChangeRecord } from "~/services/realtime/runChangeNotifierInstance.server";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { ServiceValidationError } from "~/v3/services/common.server";
import { applyMetadataMutationToBufferedRun } from "~/v3/mollifier/applyMetadataMutation.server";
+import { routeOperationsToRun } from "~/v3/mollifier/routeOperationsToRun.server";
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
import { runStore } from "~/v3/runStore.server";
@@ -67,114 +64,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
return json({ error: "Run not found" }, { status: 404 });
}
-// Route parent/root operations to the existing PG service by directly
-// invoking it against the parent/root runId. The service ingests via
-// its batching worker, which targets PG by id. If the parent/root is
-// itself buffered we recurse through our buffered-mutation helper.
-// `_ingestion_only` flag: a synthetic body that has the operations
-// promoted to top-level `operations` so the service applies them to
-// `targetRunId` directly.
-// Exported so the silent-failure logging behaviour can be unit-tested.
-// The route handler itself isn't an attractive test target (createActionApiRoute
-// wraps it in auth + body parsing + error-handler middleware), but the
-// fan-out helper carries the load-bearing logic — including the ops-
-// visibility branch this change adds.
-export async function routeOperationsToRun(
- targetRunId: string | undefined,
- operations: RunMetadataChangeOperation[] | undefined,
- env: AuthenticatedEnvironment
-): Promise {
- if (!targetRunId || !operations || operations.length === 0) return;
-
- // Try PG first via the existing service (this is how parent/root
- // operations have always landed; preserve that). Accepts the full
- // AuthenticatedEnvironment so we don't have to recover the unsafe
- // `as unknown` cast that the previous narrowed `{ id, organizationId }`
- // signature forced on us.
- //
- // Two non-success outcomes from `call`:
- // * throws — PG threw (e.g. "Cannot update metadata for a completed
- // run", or a transient PG outage).
- // * resolves with undefined — PG row didn't exist (the target may be
- // buffered, not yet materialised).
- // Either way we want to try the buffer fallback below; treating the
- // undefined-return as success would make the fallback unreachable.
- const [error, result] = await tryCatch(
- updateMetadataService.call(targetRunId, { operations }, env)
- );
- if (!error && result !== undefined) {
- // The parent/root run changed too — wake its live feeds (only when something was
- // actually written here; buffered writes publish from the flusher).
- if (result.updatedAtMs !== undefined) {
- publishChangeRecord({
- runId: result.runId,
- envId: env.id,
- tags: result.runTags,
- batchId: result.batchId,
- updatedAtMs: result.updatedAtMs,
- });
- }
- return;
- }
-
- if (error) {
- // PG threw — auxiliary op, stay best-effort and don't surface this
- // to the caller (the caller's primary mutation already landed). But
- // warn so a genuine PG outage on these ops isn't invisible.
- logger.warn("metadata route: parent/root PG op failed", {
- targetRunId,
- error: error instanceof Error ? error.message : String(error),
- });
- }
-
- // Buffer fallback only makes sense for friendlyId-keyed entries. The
- // PG-side parent/root IDs are internal cuids; the buffer keys entries
- // by friendlyId, so passing the internal id would silently no-op.
- // Skip explicitly — a buffered child's parent is always materialised
- // in PG already (a buffered run hasn't executed, so it can't have
- // triggered the child), so the buffered-parent branch isn't actually
- // reachable. Treating the no-op as intentional rather than incidental.
- if (!targetRunId.startsWith("run_")) return;
-
- // Best-effort buffer fallback. Wrap so a transient Redis throw on
- // this auxiliary op can't 500 the request after the primary mutation
- // already succeeded.
- const [bufferError, bufferOutcome] = await tryCatch(
- applyMetadataMutationToBufferedRun({
- runId: targetRunId,
- environmentId: env.id,
- organizationId: env.organizationId,
- maximumSize: appEnv.TASK_RUN_METADATA_MAXIMUM_SIZE,
- maxRetries: appEnv.TRIGGER_MOLLIFIER_METADATA_MAX_RETRIES,
- backoffBaseMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_BASE_MS,
- backoffStepMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_STEP_MS,
- body: { operations },
- })
- );
- if (bufferError) {
- logger.warn("metadata route: buffer fallback for parent/root op failed", {
- targetRunId,
- error: bufferError instanceof Error ? bufferError.message : String(bufferError),
- });
- return;
- }
- // `applyMetadataMutationToBufferedRun` reports non-throw failures via
- // its returned outcome kind: `not_found`, `busy`, `version_exhausted`,
- // `metadata_too_large`. Without inspecting `.kind`, the parent/root
- // operation can silently disappear — no PG row landed it (handled
- // above) and the buffer rejected it for one of these reasons but the
- // helper returned cleanly. Surface a warn log per non-success branch
- // so ops can trace why a parent/root op went missing. The customer's
- // primary mutation has already succeeded by this point; this remains
- // best-effort, so we still don't bubble these to the response.
- if (bufferOutcome && bufferOutcome.kind !== "applied") {
- logger.warn("metadata route: parent/root buffer op did not apply", {
- targetRunId,
- kind: bufferOutcome.kind,
- });
- }
-}
-
const { action } = createActionApiRoute(
{
params: ParamsSchema,
diff --git a/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts b/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts
index ba70f08daae..bfe609b7a85 100644
--- a/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts
+++ b/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts
@@ -40,7 +40,7 @@ function sessionResource(
return anyResource([...ids].map((id) => ({ type: "sessions" as const, id })));
}
-export const { action } = createActionApiRoute(
+const route = createActionApiRoute(
{
...routeConfig,
method: "PUT",
@@ -94,3 +94,5 @@ export const loader = createLoaderApiRoute(
return json({ presignedUrl: signed.url });
}
);
+
+export const action = route.action;
diff --git a/apps/webapp/app/routes/auth.github.callback.tsx b/apps/webapp/app/routes/auth.github.callback.tsx
index 32c23ab665b..28d5deb706c 100644
--- a/apps/webapp/app/routes/auth.github.callback.tsx
+++ b/apps/webapp/app/routes/auth.github.callback.tsx
@@ -9,12 +9,12 @@ import { commitAuthenticatedSession } from "~/services/sessionDuration.server";
import { trackAndClearReferralSource } from "~/services/referralSource.server";
import { appendRedirectTo, ssoRedirectFromAuthError } from "~/services/ssoAutoDiscovery.server";
import type { AuthUser } from "~/services/authUser";
-import { redirectCookie } from "./auth.github";
+import { githubRedirectCookie } from "~/services/redirectCookies.server";
import { sanitizeRedirectPath } from "~/utils";
export let loader: LoaderFunction = async ({ request }) => {
const cookie = request.headers.get("Cookie");
- const redirectValue = await redirectCookie.parse(cookie);
+ const redirectValue = await githubRedirectCookie.parse(cookie);
const redirectTo = sanitizeRedirectPath(redirectValue);
// The SSO auto-discovery gate runs inside the strategy's verify
diff --git a/apps/webapp/app/routes/auth.github.ts b/apps/webapp/app/routes/auth.github.ts
index 8c464e0e598..04bd5fe1284 100644
--- a/apps/webapp/app/routes/auth.github.ts
+++ b/apps/webapp/app/routes/auth.github.ts
@@ -1,6 +1,6 @@
-import { type ActionFunction, type LoaderFunction, redirect, createCookie } from "@remix-run/node";
+import { type ActionFunction, type LoaderFunction, redirect } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
-import { env } from "~/env.server";
+import { githubRedirectCookie } from "~/services/redirectCookies.server";
import { sanitizeRedirectPath } from "~/utils";
export let loader: LoaderFunction = () => redirect("/login");
@@ -23,15 +23,8 @@ export let action: ActionFunction = async ({ request }) => {
if (error instanceof Response) {
// we need to append a Set-Cookie header with a cookie storing the
// returnTo value (store the sanitized path)
- error.headers.append("Set-Cookie", await redirectCookie.serialize(safeRedirect));
+ error.headers.append("Set-Cookie", await githubRedirectCookie.serialize(safeRedirect));
}
throw error;
}
};
-
-export const redirectCookie = createCookie("redirect-to", {
- maxAge: 60 * 60, // 1 hour
- httpOnly: true,
- sameSite: "lax",
- secure: env.NODE_ENV === "production",
-});
diff --git a/apps/webapp/app/routes/auth.google.callback.tsx b/apps/webapp/app/routes/auth.google.callback.tsx
index 593b9d40fb8..e32dd43384f 100644
--- a/apps/webapp/app/routes/auth.google.callback.tsx
+++ b/apps/webapp/app/routes/auth.google.callback.tsx
@@ -9,12 +9,12 @@ import { commitAuthenticatedSession } from "~/services/sessionDuration.server";
import { trackAndClearReferralSource } from "~/services/referralSource.server";
import { appendRedirectTo, ssoRedirectFromAuthError } from "~/services/ssoAutoDiscovery.server";
import type { AuthUser } from "~/services/authUser";
-import { redirectCookie } from "./auth.google";
+import { googleRedirectCookie } from "~/services/redirectCookies.server";
import { sanitizeRedirectPath } from "~/utils";
export let loader: LoaderFunction = async ({ request }) => {
const cookie = request.headers.get("Cookie");
- const redirectValue = await redirectCookie.parse(cookie);
+ const redirectValue = await googleRedirectCookie.parse(cookie);
const redirectTo = sanitizeRedirectPath(redirectValue);
// The SSO auto-discovery gate runs inside the strategy's verify
diff --git a/apps/webapp/app/routes/auth.google.ts b/apps/webapp/app/routes/auth.google.ts
index 95fb4ff7b58..09ab022bd9f 100644
--- a/apps/webapp/app/routes/auth.google.ts
+++ b/apps/webapp/app/routes/auth.google.ts
@@ -1,6 +1,6 @@
-import { type ActionFunction, type LoaderFunction, redirect, createCookie } from "@remix-run/node";
+import { type ActionFunction, type LoaderFunction, redirect } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
-import { env } from "~/env.server";
+import { googleRedirectCookie } from "~/services/redirectCookies.server";
import { sanitizeRedirectPath } from "~/utils";
export let loader: LoaderFunction = () => redirect("/login");
@@ -23,15 +23,8 @@ export let action: ActionFunction = async ({ request }) => {
if (error instanceof Response) {
// we need to append a Set-Cookie header with a cookie storing the
// returnTo value (store the sanitized path)
- error.headers.append("Set-Cookie", await redirectCookie.serialize(safeRedirect));
+ error.headers.append("Set-Cookie", await googleRedirectCookie.serialize(safeRedirect));
}
throw error;
}
};
-
-export const redirectCookie = createCookie("google-redirect-to", {
- maxAge: 60 * 60, // 1 hour
- httpOnly: true,
- sameSite: "lax",
- secure: env.NODE_ENV === "production",
-});
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
index 4c8f77a582a..6db0b6ff566 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
@@ -1226,7 +1226,7 @@ function RunError({ error }: { error: TaskRunError }) {
{name}
{enhancedError.message && (
-
+
{enhancedError.message}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
index 7a5f61a48dc..b05d22439da 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
@@ -1125,7 +1125,7 @@ function VercelSettingsPanel({
Failed to load Vercel settings
-
+
There was an error loading the Vercel integration settings. Please refresh the page to
try again.
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
index 7414960f47a..e7dd8feec39 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
@@ -759,7 +759,7 @@ export function TierEnterprise() {
+
Contact us
}
diff --git a/apps/webapp/app/services/dashboardPreferences.server.ts b/apps/webapp/app/services/dashboardPreferences.server.ts
index 7af007dc381..46bb393224b 100644
--- a/apps/webapp/app/services/dashboardPreferences.server.ts
+++ b/apps/webapp/app/services/dashboardPreferences.server.ts
@@ -25,6 +25,7 @@ export type { SideMenuSectionId };
const DashboardPreferences = z.object({
version: z.literal("1"),
+ theme: z.enum(["dark", "light"]).optional(),
currentProjectId: z.string().optional(),
projects: z.record(
z.string(),
@@ -101,6 +102,37 @@ export async function updateCurrentProjectEnvironmentId({
});
}
+export async function updateThemePreference({
+ user,
+ theme,
+}: {
+ user: UserFromSession;
+ theme: "dark" | "light";
+}) {
+ if (user.isImpersonating) {
+ return;
+ }
+
+ if (user.dashboardPreferences.theme === theme) {
+ return;
+ }
+
+ // Narrow jsonb_set write: a full-blob update from the session snapshot can
+ // race with other preference writes and drop unrelated fields.
+ return prisma.$executeRaw`
+ UPDATE "User"
+ SET "dashboardPreferences" = jsonb_set(
+ COALESCE(
+ "dashboardPreferences",
+ '{"version":"1","projects":{}}'::jsonb
+ ),
+ '{theme}',
+ to_jsonb(${theme}::text)
+ )
+ WHERE id = ${user.id}
+ `;
+}
+
export async function clearCurrentProject({ user }: { user: UserFromSession }) {
if (user.isImpersonating) {
return;
diff --git a/apps/webapp/app/services/redirectCookies.server.ts b/apps/webapp/app/services/redirectCookies.server.ts
new file mode 100644
index 00000000000..58cecbc3398
--- /dev/null
+++ b/apps/webapp/app/services/redirectCookies.server.ts
@@ -0,0 +1,19 @@
+import { createCookie } from "@remix-run/node";
+import { env } from "~/env.server";
+
+// Post-auth redirect cookies. Kept in a .server module: Vite can't strip
+// non-standard route exports that pull in server-only code.
+
+export const githubRedirectCookie = createCookie("redirect-to", {
+ maxAge: 60 * 60, // 1 hour
+ httpOnly: true,
+ sameSite: "lax",
+ secure: env.NODE_ENV === "production",
+});
+
+export const googleRedirectCookie = createCookie("google-redirect-to", {
+ maxAge: 60 * 60, // 1 hour
+ httpOnly: true,
+ sameSite: "lax",
+ secure: env.NODE_ENV === "production",
+});
diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css
index f7b05430c89..6ef03402328 100644
--- a/apps/webapp/app/tailwind.css
+++ b/apps/webapp/app/tailwind.css
@@ -1,6 +1,3 @@
-@import url("non.geist");
-@import url("non.geist/mono");
-
@import "react-grid-layout/css/styles.css" layer(base);
@import "react-resizable/css/styles.css" layer(base);
@@ -161,6 +158,7 @@
--color-surface-control: var(--color-charcoal-600);
--color-surface-control-hover: var(--color-charcoal-550);
--color-surface-control-active: var(--color-charcoal-500);
+ --color-input-bg: var(--color-charcoal-750);
/* Borders, from subtlest to most visible */
--color-grid-dimmed: var(--color-charcoal-750);
@@ -602,3 +600,175 @@
@apply leading-relaxed;
}
}
+
+/*
+ Light theme. Overrides the themable variables only; raw palettes stay put.
+ Code/editor values come from the trigger.light VS Code theme.
+*/
+[data-theme="light"] {
+ --color-secondary: #ffffff;
+ --color-input-bg: #ffffff;
+
+ /* shadcn vars consumed by charts/streamdown (tooltip cursor fill etc.) */
+ --background: #ffffff;
+ --foreground: #1a1b1f;
+ --muted: #eceef1;
+ --muted-foreground: #5f6570;
+ --border: #e2e4e9;
+ --sidebar: #f6f7f8;
+ --primary-foreground: #ffffff;
+
+ /* Text */
+ --color-primary: var(--color-apple-600);
+ --color-tertiary: #eef0f3;
+ --color-text-link: var(--color-lavender-600);
+ --color-text-faint: var(--color-charcoal-400);
+ --color-text-dimmed: var(--color-charcoal-500);
+ --color-text-bright: var(--color-charcoal-800);
+
+ /* Surfaces */
+ --color-background-deep: #f1f2f4;
+ --color-background-dimmed: #fbfbfc;
+ --color-background-bright: #ffffff;
+ --color-background-hover: #f2f3f5;
+ --color-background-raised: #e9eaee;
+ --color-surface-control: #dcdee3;
+ --color-surface-control-hover: #cfd2d9;
+ --color-surface-control-active: #b8bcc6;
+
+ /* Borders */
+ --color-grid-dimmed: #eceef1;
+ --color-grid-bright: #e2e4e9;
+ --color-border-bright: #d2d5db;
+ --color-border-brighter: #b9bdc7;
+ --color-border-brightest: #9ba1ad;
+
+ /* Status - darker steps for contrast on white */
+ --color-success: var(--color-mint-600);
+ --color-warning: var(--color-amber-600);
+ --color-dev: var(--color-pink-600);
+ --color-prod: var(--color-mint-600);
+ --color-staging: var(--color-orange-600);
+ --color-preview: var(--color-yellow-700);
+
+ /* Neutral run statuses: soft light grays for sparkbars and charts */
+ --color-run-pending: #ccd0d6;
+ --color-run-delayed: #d3d6db;
+ --color-run-waiting-to-resume: #c5c9d0;
+ --color-run-canceled: #d8dbdf;
+ --color-run-expired: #dee0e4;
+
+ /* Icons that fail contrast on white */
+ --color-schedules: var(--color-yellow-600);
+ --color-previewBranches: var(--color-yellow-600);
+ --color-customDashboards: var(--color-charcoal-500);
+
+ /* Callouts - deep tints instead of the dark theme's pastels */
+ --color-callout-warning-text: var(--color-yellow-800);
+ --color-callout-error-text: var(--color-rose-700);
+ --color-callout-success-text: var(--color-green-800);
+ --color-callout-docs-text: var(--color-blue-800);
+ --color-callout-pending-bg: var(--color-blue-100);
+ --color-callout-pending-text: var(--color-blue-800);
+ --color-callout-pricing-bg: var(--color-indigo-100);
+ --color-callout-pricing-text: var(--color-indigo-800);
+
+ /* Code syntax - trigger.light */
+ --color-code-background: #ffffff;
+ --color-code-foreground: #333333;
+ --color-code-line-number: #a8a8ad;
+ --color-code-plain: #2e2e4b;
+ --color-code-muted: #333333;
+ --color-code-comment: #767a81;
+ --color-code-keyword: #b114d3;
+ --color-code-storage: #b114d3;
+ --color-code-type: #b114d3;
+ --color-code-function: #6532f5;
+ --color-code-variable: #404040;
+ --color-code-constant: #1e1e1e;
+ --color-code-language: #b114d3;
+ --color-code-object-key: #222222;
+ --color-code-string: #262626;
+ --color-code-template-punctuation: #0879e2;
+ --color-code-number: #262626;
+ --color-code-builtin: #3080e0;
+ --color-code-attribute: #222222;
+ --color-code-escape: #222222;
+ --color-code-regexp: #dc3545;
+ --color-code-regexp-constant: #5f6570;
+ --color-code-invalid: #dc3545;
+ --color-code-deleted: #dc3545;
+ --color-code-jsx-text: #2e2e4b;
+
+ /* CodeMirror - trigger.light */
+ --color-editor-background: #ffffff;
+ --color-editor-foreground: #2e2e4b;
+ --color-editor-keyword: #b114d3;
+ --color-editor-name: #197c3c;
+ --color-editor-function: #6532f5;
+ --color-editor-constant: #3080e0;
+ --color-editor-type: #2980b9;
+ --color-editor-operator: #333333;
+ --color-editor-comment: #767a81;
+ --color-editor-heading: #2c3e50;
+ --color-editor-string: #262626;
+ --color-editor-invalid: #dc3545;
+ --color-editor-panel-background: #f4f4f6;
+ --color-editor-highlight-background: #f8f8fa;
+ --color-editor-tooltip-background: #ffffff;
+ --color-editor-selection: #d9dce3;
+ --color-editor-cursor: #2e2e4b;
+ --color-editor-search-match: #0879e226;
+ --color-editor-search-match-outline: #0879e2;
+ --color-editor-search-match-selected: #0879e240;
+ --color-editor-selection-match: #e8e8ed;
+ --color-editor-matching-bracket: rgba(240, 241, 244, 0.9);
+ --color-editor-matching-bracket-outline: rgba(160, 166, 180, 0.5);
+ --color-editor-fold-placeholder: #555555;
+ --color-editor-scrollbar-track-active: #e8e9ec;
+ --color-editor-scrollbar-thumb: #c9ccd4;
+ --color-editor-scrollbar-thumb-active: #aeb3be;
+}
+
+/* Streamdown's muted surface has no semantic token (charcoal-775); theme it here */
+[data-theme="light"] .streamdown-container {
+ --muted: #eceef1;
+}
+
+/* The timeline label shadow is a dark-theme legibility aid; drop it on light */
+[data-theme="light"] .text-shadow-custom {
+ text-shadow: none;
+}
+
+/* Neutral timeline points: invert to a light dot with a gray ring */
+[data-theme="light"] .timeline-point.bg-surface-control-active {
+ border-color: var(--color-surface-control-active);
+ background-color: var(--color-background-bright);
+}
+
+/* Run timeline bars: no fade gradient on light */
+[data-theme="light"] .timeline-span {
+ background-image: none;
+}
+/* On saturated bars the duration label keeps the dark-theme treatment,
+ but only when the bar is wide enough to contain the label — on narrow
+ bars the sticky label overflows onto the page background, where the
+ default dark-on-light text is correct. */
+[data-theme="light"] .timeline-span.bg-success,
+[data-theme="light"] .timeline-span.bg-error {
+ container-type: inline-size;
+}
+@container (min-width: 3.5rem) {
+ [data-theme="light"] .timeline-span.bg-success .text-shadow-custom,
+ [data-theme="light"] .timeline-span.bg-error .text-shadow-custom {
+ color: #ffffff;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+ }
+}
+
+/* The table row-hover menu wraps the always-visible "Suggest a region" button
+ in a ring container; on light that ring doubles up with the button's own
+ border, so drop it here. */
+[data-theme="light"] .suggest-region-cell > div > div {
+ box-shadow: none;
+}
diff --git a/apps/webapp/app/utils/reloadingRegistry.server.ts b/apps/webapp/app/utils/reloadingRegistry.server.ts
index 81eb6723457..3bbb9b4e8b1 100644
--- a/apps/webapp/app/utils/reloadingRegistry.server.ts
+++ b/apps/webapp/app/utils/reloadingRegistry.server.ts
@@ -3,29 +3,34 @@ import { Counter, Gauge } from "prom-client";
import { metricsRegister } from "~/metrics.server";
import { logger } from "~/services/logger.server";
import { signalsEmitter } from "~/services/signals.server";
+import { singleton } from "~/utils/singleton";
-const loadFailures = new Counter({
- name: "reloading_registry_load_failures_total",
- help: "Failed loads of a reloading registry",
- labelNames: ["name"],
- registers: [metricsRegister],
-});
-
-const lastSuccessfulLoadAt = new Gauge({
- name: "reloading_registry_last_successful_load_timestamp_seconds",
- help: "Unix time of the last successful registry load (staleness signal)",
- labelNames: ["name"],
- registers: [metricsRegister],
-});
-
-// 0 until the first successful load, then 1. Starts at 0 (not absent) so a
-// never-loaded registry is an alertable series, distinct from "feature off".
-const registryLoaded = new Gauge({
- name: "reloading_registry_loaded",
- help: "1 once the registry has loaded at least once, else 0 (0 = serving cold fallback)",
- labelNames: ["name"],
- registers: [metricsRegister],
-});
+// singleton: module-scope registrations double-register under Vite dev HMR
+const { loadFailures, lastSuccessfulLoadAt, registryLoaded } = singleton(
+ "reloadingRegistryMetrics",
+ () => ({
+ loadFailures: new Counter({
+ name: "reloading_registry_load_failures_total",
+ help: "Failed loads of a reloading registry",
+ labelNames: ["name"],
+ registers: [metricsRegister],
+ }),
+ lastSuccessfulLoadAt: new Gauge({
+ name: "reloading_registry_last_successful_load_timestamp_seconds",
+ help: "Unix time of the last successful registry load (staleness signal)",
+ labelNames: ["name"],
+ registers: [metricsRegister],
+ }),
+ // 0 until the first successful load, then 1. Starts at 0 (not absent) so a
+ // never-loaded registry is an alertable series, distinct from "feature off".
+ registryLoaded: new Gauge({
+ name: "reloading_registry_loaded",
+ help: "1 once the registry has loaded at least once, else 0 (0 = serving cold fallback)",
+ labelNames: ["name"],
+ registers: [metricsRegister],
+ }),
+ })
+);
export type ReloadingRegistry = {
isReady: Promise;
diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts
index 637830aef06..9a6270fa6d3 100644
--- a/apps/webapp/app/v3/featureFlags.ts
+++ b/apps/webapp/app/v3/featureFlags.ts
@@ -10,6 +10,7 @@ export const FEATURE_FLAG = {
hasComputeAccess: "hasComputeAccess",
hasPrivateConnections: "hasPrivateConnections",
hasSso: "hasSso",
+ hasThemeSwitcher: "hasThemeSwitcher",
mollifierEnabled: "mollifierEnabled",
workerQueueScheduledSplitEnabled: "workerQueueScheduledSplitEnabled",
realtimeBackend: "realtimeBackend",
@@ -33,6 +34,8 @@ export const FeatureFlagCatalog = {
[FEATURE_FLAG.hasComputeAccess]: z.coerce.boolean(),
[FEATURE_FLAG.hasPrivateConnections]: z.coerce.boolean(),
[FEATURE_FLAG.hasSso]: z.coerce.boolean(),
+ // Gates the Interface theme setting in /account. Off by default.
+ [FEATURE_FLAG.hasThemeSwitcher]: z.coerce.boolean(),
[FEATURE_FLAG.mollifierEnabled]: z.coerce.boolean(),
[FEATURE_FLAG.workerQueueScheduledSplitEnabled]: z.coerce.boolean(),
// Which backend serves the realtime run feed. Controllable
diff --git a/apps/webapp/app/v3/mollifier/routeOperationsToRun.server.ts b/apps/webapp/app/v3/mollifier/routeOperationsToRun.server.ts
new file mode 100644
index 00000000000..387759ff9e8
--- /dev/null
+++ b/apps/webapp/app/v3/mollifier/routeOperationsToRun.server.ts
@@ -0,0 +1,116 @@
+import { tryCatch } from "@trigger.dev/core/utils";
+import type { RunMetadataChangeOperation } from "@trigger.dev/core/v3/schemas";
+import { env as appEnv } from "~/env.server";
+import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
+import { logger } from "~/services/logger.server";
+import { updateMetadataService } from "~/services/metadata/updateMetadataInstance.server";
+import { publishChangeRecord } from "~/services/realtime/runChangeNotifierInstance.server";
+import { applyMetadataMutationToBufferedRun } from "./applyMetadataMutation.server";
+
+// Route parent/root operations to the existing PG service by directly
+// invoking it against the parent/root runId. The service ingests via
+// its batching worker, which targets PG by id. If the parent/root is
+// itself buffered we recurse through our buffered-mutation helper.
+// `_ingestion_only` flag: a synthetic body that has the operations
+// promoted to top-level `operations` so the service applies them to
+// `targetRunId` directly.
+// Exported so the silent-failure logging behaviour can be unit-tested.
+// The route handler itself isn't an attractive test target (createActionApiRoute
+// wraps it in auth + body parsing + error-handler middleware), but the
+// fan-out helper carries the load-bearing logic — including the ops-
+// visibility branch this change adds.
+export async function routeOperationsToRun(
+ targetRunId: string | undefined,
+ operations: RunMetadataChangeOperation[] | undefined,
+ env: AuthenticatedEnvironment
+): Promise {
+ if (!targetRunId || !operations || operations.length === 0) return;
+
+ // Try PG first via the existing service (this is how parent/root
+ // operations have always landed; preserve that). Accepts the full
+ // AuthenticatedEnvironment so we don't have to recover the unsafe
+ // `as unknown` cast that the previous narrowed `{ id, organizationId }`
+ // signature forced on us.
+ //
+ // Two non-success outcomes from `call`:
+ // * throws — PG threw (e.g. "Cannot update metadata for a completed
+ // run", or a transient PG outage).
+ // * resolves with undefined — PG row didn't exist (the target may be
+ // buffered, not yet materialised).
+ // Either way we want to try the buffer fallback below; treating the
+ // undefined-return as success would make the fallback unreachable.
+ const [error, result] = await tryCatch(
+ updateMetadataService.call(targetRunId, { operations }, env)
+ );
+ if (!error && result !== undefined) {
+ // The parent/root run changed too — wake its live feeds (only when something was
+ // actually written here; buffered writes publish from the flusher).
+ if (result.updatedAtMs !== undefined) {
+ publishChangeRecord({
+ runId: result.runId,
+ envId: env.id,
+ tags: result.runTags,
+ batchId: result.batchId,
+ updatedAtMs: result.updatedAtMs,
+ });
+ }
+ return;
+ }
+
+ if (error) {
+ // PG threw — auxiliary op, stay best-effort and don't surface this
+ // to the caller (the caller's primary mutation already landed). But
+ // warn so a genuine PG outage on these ops isn't invisible.
+ logger.warn("metadata route: parent/root PG op failed", {
+ targetRunId,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+
+ // Buffer fallback only makes sense for friendlyId-keyed entries. The
+ // PG-side parent/root IDs are internal cuids; the buffer keys entries
+ // by friendlyId, so passing the internal id would silently no-op.
+ // Skip explicitly — a buffered child's parent is always materialised
+ // in PG already (a buffered run hasn't executed, so it can't have
+ // triggered the child), so the buffered-parent branch isn't actually
+ // reachable. Treating the no-op as intentional rather than incidental.
+ if (!targetRunId.startsWith("run_")) return;
+
+ // Best-effort buffer fallback. Wrap so a transient Redis throw on
+ // this auxiliary op can't 500 the request after the primary mutation
+ // already succeeded.
+ const [bufferError, bufferOutcome] = await tryCatch(
+ applyMetadataMutationToBufferedRun({
+ runId: targetRunId,
+ environmentId: env.id,
+ organizationId: env.organizationId,
+ maximumSize: appEnv.TASK_RUN_METADATA_MAXIMUM_SIZE,
+ maxRetries: appEnv.TRIGGER_MOLLIFIER_METADATA_MAX_RETRIES,
+ backoffBaseMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_BASE_MS,
+ backoffStepMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_STEP_MS,
+ body: { operations },
+ })
+ );
+ if (bufferError) {
+ logger.warn("metadata route: buffer fallback for parent/root op failed", {
+ targetRunId,
+ error: bufferError instanceof Error ? bufferError.message : String(bufferError),
+ });
+ return;
+ }
+ // `applyMetadataMutationToBufferedRun` reports non-throw failures via
+ // its returned outcome kind: `not_found`, `busy`, `version_exhausted`,
+ // `metadata_too_large`. Without inspecting `.kind`, the parent/root
+ // operation can silently disappear — no PG row landed it (handled
+ // above) and the buffer rejected it for one of these reasons but the
+ // helper returned cleanly. Surface a warn log per non-success branch
+ // so ops can trace why a parent/root op went missing. The customer's
+ // primary mutation has already succeeded by this point; this remains
+ // best-effort, so we still don't bubble these to the response.
+ if (bufferOutcome && bufferOutcome.kind !== "applied") {
+ logger.warn("metadata route: parent/root buffer op did not apply", {
+ targetRunId,
+ kind: bufferOutcome.kind,
+ });
+ }
+}
diff --git a/apps/webapp/app/v3/querySchemas.ts b/apps/webapp/app/v3/querySchemas.ts
index 4784ad75629..a860d411a37 100644
--- a/apps/webapp/app/v3/querySchemas.ts
+++ b/apps/webapp/app/v3/querySchemas.ts
@@ -2,7 +2,6 @@ import { column, type BucketThreshold, type TableSchema } from "@internal/tsql";
import { z } from "zod";
import { autoFormatSQL } from "~/components/code/TSQLEditor";
import { runFriendlyStatus, runStatusTitleFromStatus } from "~/components/runs/v3/TaskRunStatus";
-import { logger } from "~/services/logger.server";
export const QueryScopeSchema = z.enum(["organization", "project", "environment"]);
export type QueryScope = z.infer;
@@ -443,10 +442,7 @@ export const runsSchema: TableSchema = {
...column("Array(String)", {
description: "Any bulk actions that operated on this run.",
example: '["bulk_12345678", "bulk_34567890"]',
- whereTransform: (value: string) => {
- logger.log(`WHERE TRANSFORM: ${value}`);
- return value.replace(/^bulk_/, "");
- },
+ whereTransform: (value: string) => value.replace(/^bulk_/, ""),
}),
},
},
diff --git a/apps/webapp/package.json b/apps/webapp/package.json
index ea7352cb3c1..c98376d754f 100644
--- a/apps/webapp/package.json
+++ b/apps/webapp/package.json
@@ -5,10 +5,10 @@
"sideEffects": false,
"scripts": {
"build": "run-s build:** && pnpm run upload:sourcemaps",
- "build:remix": "remix build --sourcemap",
+ "build:remix": "remix vite:build",
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build --sourcemap",
"build:sentry": "esbuild --platform=node --format=cjs --outbase=. ./sentry.server.ts ./app/utils/sentryTraceContext.server.ts --outdir=build --sourcemap",
- "dev": "cross-env PORT=3030 remix dev -c \"node ./build/server.js\"",
+ "dev": "cross-env NODE_ENV=development PORT=3030 tsx ./server.ts",
"dev:worker": "cross-env NODE_PATH=../../node_modules/.pnpm/node_modules node ./build/server.js",
"format": "oxfmt .",
"lint": "oxlint -c ../../.oxlintrc.json",
@@ -141,6 +141,7 @@
"@whatwg-node/fetch": "^0.9.14",
"@window-splitter/react": "1.1.3",
"ai": "^6.0.116",
+ "assert": "^2.1.0",
"assert-never": "^1.2.1",
"aws4fetch": "^1.0.18",
"class-variance-authority": "^0.5.2",
@@ -184,6 +185,7 @@
"p-map": "^6.0.0",
"p-retry": "^4.6.1",
"parse-duration": "^2.1.0",
+ "pg": "8.15.6",
"posthog-js": "^1.93.3",
"posthog-node": "5.35.6",
"prism-react-renderer": "^2.3.1",
@@ -227,10 +229,11 @@
"superjson": "^2.2.1",
"tailwind-merge": "^3.6.0",
"tailwind-scrollbar-hide": "^4.0.0",
- "tw-animate-css": "^1.4.0",
"tiny-invariant": "^1.2.0",
+ "tw-animate-css": "^1.4.0",
"ulid": "^2.3.0",
"ulidx": "^2.2.1",
+ "util": "^0.12.5",
"uuid": "^14.0.0",
"ws": "^8.11.0",
"zod": "3.25.76",
@@ -291,7 +294,8 @@
"tailwindcss": "^4.3.1",
"tsconfig-paths": "^3.14.1",
"tsx": "^4.20.6",
- "vite-tsconfig-paths": "^4.0.5"
+ "vite-tsconfig-paths": "^5.1.4",
+ "vite": "^6.4.2"
},
"engines": {
"node": ">=18.19.0 || >=20.6.0"
diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js
deleted file mode 100644
index f6bad31aa77..00000000000
--- a/apps/webapp/remix.config.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- dev: {
- port: 8002,
- },
- // Tailwind v4 runs through PostCSS (@tailwindcss/postcss), not the built-in Remix integration
- tailwind: false,
- postcss: true,
- cacheDirectory: "./node_modules/.cache/remix",
- ignoredRouteFiles: ["**/.*"],
- serverModuleFormat: "cjs",
- serverDependenciesToBundle: [
- /^remix-utils.*/,
- /^@internal\//, // Bundle all internal packages
- /^@trigger\.dev\//, // Bundle all trigger packages
- "marked",
- "agentcrumbs",
- "axios",
- "p-limit",
- "p-map",
- "yocto-queue",
- "@unkey/cache",
- "@unkey/cache/stores",
- "emails",
- "highlight.run",
- "random-words",
- "superjson",
- "copy-anything",
- "is-what",
- "prismjs/components/prism-json",
- "prismjs/components/prism-typescript",
- "redlock",
- "parse-duration",
- "uncrypto",
- "std-env",
- "uuid",
- ],
- browserNodeBuiltinsPolyfill: {
- modules: {
- path: true,
- os: true,
- crypto: true,
- http2: true,
- assert: true,
- util: true,
- },
- },
-};
diff --git a/apps/webapp/remix.env.d.ts b/apps/webapp/remix.env.d.ts
index 72e2affe311..3a7ebcc0835 100644
--- a/apps/webapp/remix.env.d.ts
+++ b/apps/webapp/remix.env.d.ts
@@ -1,2 +1,3 @@
///
+///
///
diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts
index c3c1a45f633..93c0cab917b 100644
--- a/apps/webapp/server.ts
+++ b/apps/webapp/server.ts
@@ -1,13 +1,13 @@
import "./sentry.server";
import { createRequestHandler } from "@remix-run/express";
-import { broadcastDevReady, logDevReady } from "@remix-run/server-runtime";
import compression from "compression";
import type { Server as EngineServer } from "engine.io";
import express, { type RequestHandler } from "express";
import morgan from "morgan";
import { nanoid } from "nanoid";
import path from "path";
+import { pathToFileURL } from "node:url";
import type { Server as IoServer } from "socket.io";
import type { WebSocketServer } from "ws";
import type { RateLimitMiddleware } from "~/services/apiRateLimit.server";
@@ -74,6 +74,12 @@ function installPrimarySignalHandlers() {
});
}
+// Bundled to CJS (esbuild rewrites import() to require); vite and the Remix
+// server bundle are ESM, so load them via a real dynamic import.
+const dynamicImport = new Function("specifier", "return import(specifier)") as (
+ specifier: string
+) => Promise;
+
if (ENABLE_CLUSTER && cluster.isPrimary) {
process.title = `node webapp-server primary`;
console.log(`[cluster] Primary ${process.pid} is starting with ${WORKERS} workers`);
@@ -92,6 +98,13 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
installPrimarySignalHandlers();
} else {
+ startServer().catch((error) => {
+ console.error("Failed to start server:", error);
+ process.exit(1);
+ });
+}
+
+async function startServer() {
const app = express();
if (process.env.DISABLE_COMPRESSION !== "1") {
@@ -101,16 +114,25 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable("x-powered-by");
- // Remix fingerprints its assets so we can cache forever.
- app.use("/build", express.static("public/build", { immutable: true, maxAge: "1y" }));
- // Stale dev builds can request an old hashed manifest; don't fall through to Remix.
- app.use("/build", (_req, res) => {
- res.status(404).end();
- });
+ const MODE = process.env.NODE_ENV;
- // Everything else (like favicon.ico) is cached for an hour. You may want to be
- // more aggressive with this caching.
- app.use(express.static("public", { maxAge: "1h" }));
+ // In development, Vite serves assets (and handles HMR) via middleware.
+ const viteDevServer =
+ MODE === "production"
+ ? undefined
+ : await dynamicImport("vite").then((vite) =>
+ vite.createServer({ server: { middlewareMode: true } })
+ );
+
+ if (viteDevServer) {
+ app.use(viteDevServer.middlewares);
+ } else {
+ // Vite fingerprints its assets so we can cache forever.
+ app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }));
+ // Everything else (like favicon.ico) is cached for an hour. You may want to be
+ // more aggressive with this caching.
+ app.use(express.static("build/client", { maxAge: "1h" }));
+ }
// On high-volume machine-ingest services (e.g. otel) the per-request access
// log dominates log volume. HTTP_ACCESS_LOG_DISABLED suppresses successful
@@ -127,9 +149,17 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
? `node webapp-worker-${cluster.isWorker ? cluster.worker?.id : "solo"}`
: "node webapp-server";
- const MODE = process.env.NODE_ENV;
- const BUILD_DIR = path.join(process.cwd(), "build");
- const build = require(BUILD_DIR);
+ const loadBuild = () => {
+ if (viteDevServer) {
+ return viteDevServer.ssrLoadModule("virtual:remix/server-build");
+ }
+ return dynamicImport(
+ pathToFileURL(path.join(process.cwd(), "build", "server", "index.mjs")).href
+ );
+ };
+
+ // Boots the entry.server singletons (socket.io, wss, rate limiters).
+ const build = await loadBuild();
const port = process.env.REMIX_APP_PORT || process.env.PORT || 3000;
@@ -196,7 +226,7 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
"*",
// @ts-ignore
createRequestHandler({
- build,
+ build: viteDevServer ? loadBuild : build,
mode: MODE,
})
);
@@ -209,7 +239,7 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
"/healthcheck",
// @ts-ignore
createRequestHandler({
- build,
+ build: viteDevServer ? loadBuild : build,
mode: MODE,
})
);
@@ -221,12 +251,6 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
ENABLE_CLUSTER && cluster.isWorker ? ` [worker ${cluster.worker?.id}/${process.pid}]` : ""
}`
);
-
- if (MODE === "development") {
- broadcastDevReady(build)
- .then(() => logDevReady(build))
- .catch(console.error);
- }
});
server.keepAliveTimeout = HTTP_KEEPALIVE_TIMEOUT_MS;
@@ -248,6 +272,8 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
console.log("Express server closed gracefully.");
}
});
+ // Dev-only: release Vite's file watchers and HMR websocket
+ viteDevServer?.close();
}
process.on("SIGTERM", closeServer);
@@ -293,7 +319,6 @@ if (ENABLE_CLUSTER && cluster.isPrimary) {
});
});
} else {
- require(BUILD_DIR);
console.log(`✅ app ready (skipping http server)`);
}
}
diff --git a/apps/webapp/test/metadataRouteOperationsLogging.test.ts b/apps/webapp/test/metadataRouteOperationsLogging.test.ts
index b7e5f860198..588c1547ed4 100644
--- a/apps/webapp/test/metadataRouteOperationsLogging.test.ts
+++ b/apps/webapp/test/metadataRouteOperationsLogging.test.ts
@@ -52,7 +52,7 @@ vi.mock("~/services/logger.server", () => ({
},
}));
-import { routeOperationsToRun } from "~/routes/api.v1.runs.$runId.metadata";
+import { routeOperationsToRun } from "~/v3/mollifier/routeOperationsToRun.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
const env = {
diff --git a/apps/webapp/vite.config.ts b/apps/webapp/vite.config.ts
new file mode 100644
index 00000000000..b008ae04ed0
--- /dev/null
+++ b/apps/webapp/vite.config.ts
@@ -0,0 +1,66 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { defaultClientConditions, defaultServerConditions, defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [
+ remix({
+ ignoredRouteFiles: ["**/.*"],
+ // .mjs so the CJS server.ts wrapper can dynamic-import it
+ serverBuildFile: "index.mjs",
+ }),
+ tsconfigPaths(),
+ ],
+ resolve: {
+ // Resolve workspace packages to TS source (same condition the CLI uses)
+ conditions: ["@triggerdotdev/source", ...defaultClientConditions],
+ // Browser polyfills for node builtins used by client deps (antlr4ts)
+ alias: [
+ { find: /^assert$/, replacement: "assert/" },
+ { find: /^util$/, replacement: "util/" },
+ ],
+ },
+ optimizeDeps: {
+ // Crawl all routes up front - mid-session re-optimization duplicates React
+ entries: ["./app/entry.client.tsx", "./app/root.tsx", "./app/routes/**/*.{ts,tsx}"],
+ esbuildOptions: {
+ // node globals for prebundled CJS deps (client-only by construction)
+ define: { global: "globalThis" },
+ inject: ["./vite/node-globals-shim.js"],
+ },
+ },
+ server: {
+ warmup: {
+ clientFiles: ["./app/entry.client.tsx", "./app/root.tsx", "./app/components/**/*.tsx"],
+ ssrFiles: ["./app/entry.server.tsx", "./app/root.tsx"],
+ },
+ },
+ build: {
+ sourcemap: true,
+ rollupOptions: {
+ // Prisma wrappers and pg have CJS/native pieces Rollup can't inline
+ external: [/^@trigger\.dev\/database$/, /^@internal\/run-ops-database$/, /^pg$/],
+ },
+ },
+ ssr: {
+ resolve: {
+ conditions: ["@triggerdotdev/source", ...defaultServerConditions],
+ externalConditions: ["@triggerdotdev/source", "node"],
+ },
+ // CJS Prisma clients and native pg must load through node
+ external: ["@trigger.dev/database", "@internal/run-ops-database", "pg"],
+ // CJS deps whose named exports node's ESM interop can't detect
+ noExternal: [
+ /^@radix-ui\//,
+ "react-use",
+ "cron-parser",
+ "@fingerprintjs/fingerprintjs-pro-react",
+ "@kapaai/react-sdk",
+ "@fingerprintjs/fingerprintjs-pro",
+ "@fingerprintjs/fingerprintjs-pro-spa",
+ ],
+ optimizeDeps: {
+ include: ["cron-parser"],
+ },
+ },
+});
diff --git a/apps/webapp/vite/node-globals-shim.js b/apps/webapp/vite/node-globals-shim.js
new file mode 100644
index 00000000000..68d6826c862
--- /dev/null
+++ b/apps/webapp/vite/node-globals-shim.js
@@ -0,0 +1,10 @@
+// Minimal `process` stand-in injected into prebundled browser deps
+// (see vite.config.ts optimizeDeps). Client-only.
+export const process = {
+ env: {},
+ browser: true,
+ version: "",
+ platform: "browser",
+ cwd: () => "/",
+ nextTick: (fn, ...args) => setTimeout(() => fn(...args), 0),
+};
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 5b140b037f3..2e94f121498 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -91,6 +91,12 @@ COPY --from=dev-deps --chown=node:node /triggerdotdev/internal-packages/database
# Run-ops Prisma client (query engine + client). Only constructed when the split is enabled,
# but the image must carry it so a split-on deployment doesn't hit a missing query engine.
COPY --from=dev-deps --chown=node:node /triggerdotdev/internal-packages/run-ops-database/generated ./internal-packages/run-ops-database/generated
+# These workspace packages stay external to the Vite SSR bundle, so their
+# built entry points must ship in the image (core is the sdk's runtime dep).
+COPY --from=builder --chown=node:node /triggerdotdev/internal-packages/database/dist ./internal-packages/database/dist
+COPY --from=builder --chown=node:node /triggerdotdev/internal-packages/run-ops-database/dist ./internal-packages/run-ops-database/dist
+COPY --from=builder --chown=node:node /triggerdotdev/packages/trigger-sdk/dist ./packages/trigger-sdk/dist
+COPY --from=builder --chown=node:node /triggerdotdev/packages/core/dist ./packages/core/dist
COPY --from=builder --chown=node:node /triggerdotdev/apps/webapp/build/server.js ./apps/webapp/build/server.js
COPY --from=builder --chown=node:node /triggerdotdev/apps/webapp/build ./apps/webapp/build
COPY --from=builder --chown=node:node /triggerdotdev/apps/webapp/public ./apps/webapp/public
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 46574c2677c..bc682658d21 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -231,10 +231,10 @@ services:
"--query",
"SELECT 1",
]
- interval: "3s"
- timeout: "5s"
- retries: "5"
- start_period: "10s"
+ interval: 3s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
clickhouse_migrator:
build:
diff --git a/internal-packages/rbac/src/index.ts b/internal-packages/rbac/src/index.ts
index 258dc12f3b1..dcf046f9ab3 100644
--- a/internal-packages/rbac/src/index.ts
+++ b/internal-packages/rbac/src/index.ts
@@ -85,7 +85,8 @@ class LazyController implements RoleBaseAccessController {
}
const moduleName = "@triggerdotdev/plugins/rbac";
try {
- const module = await import(moduleName);
+ // Optional plugin, resolved at runtime only
+ const module = await import(/* @vite-ignore */ moduleName);
const plugin: RoleBasedAccessControlPlugin = module.default;
console.log("RBAC: using plugin implementation");
return plugin.create({ userActorSecret: options?.userActorSecret });
diff --git a/internal-packages/run-engine/src/engine/locking.ts b/internal-packages/run-engine/src/engine/locking.ts
index 0c86da80cc1..900b964bee6 100644
--- a/internal-packages/run-engine/src/engine/locking.ts
+++ b/internal-packages/run-engine/src/engine/locking.ts
@@ -1,8 +1,12 @@
-// import { default: Redlock } from "redlock";
-const { default: Redlock } = require("redlock");
import { AsyncLocalStorage } from "async_hooks";
import type { Redis } from "@internal/redis";
-import type * as redlock from "redlock";
+import * as redlockModule from "redlock";
+
+// redlock is CJS with `exports.default`; probe the interop shapes instead of
+// a bare require(), which breaks in ESM module runners.
+const Redlock = ((redlockModule as any).default?.default ??
+ (redlockModule as any).default ??
+ redlockModule) as typeof redlockModule.default;
import { tryCatch } from "@trigger.dev/core";
import type { Logger } from "@trigger.dev/core/logger";
import type { Tracer, Meter, ObservableResult, Attributes, Histogram } from "@internal/tracing";
@@ -34,12 +38,12 @@ export class LockAcquisitionTimeoutError extends Error {
interface LockContext {
resources: string;
- signal: redlock.RedlockAbortSignal;
+ signal: redlockModule.RedlockAbortSignal;
lockType: string;
}
interface ManualLockContext {
- lock: redlock.Lock;
+ lock: redlockModule.Lock;
timeout: NodeJS.Timeout | null | undefined;
extension: Promise | undefined;
}
@@ -60,7 +64,7 @@ export interface LockRetryConfig {
}
export class RunLocker {
- private redlock: InstanceType;
+ private redlock: InstanceType;
private asyncLocalStorage: AsyncLocalStorage;
private logger: Logger;
private tracer: Tracer;
@@ -216,7 +220,7 @@ export class RunLocker {
let totalWaitTime = 0;
// Retry the lock acquisition with exponential backoff
- let lock: redlock.Lock | undefined;
+ let lock: redlockModule.Lock | undefined;
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -346,7 +350,7 @@ export class RunLocker {
// Create an AbortController for our signal
const controller = new AbortController();
- const signal = controller.signal as redlock.RedlockAbortSignal;
+ const signal = controller.signal as redlockModule.RedlockAbortSignal;
const manualContext: ManualLockContext = {
lock,
@@ -425,7 +429,7 @@ export class RunLocker {
#setupAutoExtension(
context: ManualLockContext,
duration: number,
- signal: redlock.RedlockAbortSignal,
+ signal: redlockModule.RedlockAbortSignal,
controller: AbortController
): void {
if (this.automaticExtensionThreshold > duration - 100) {
@@ -460,7 +464,7 @@ export class RunLocker {
async #extendLock(
context: ManualLockContext,
duration: number,
- signal: redlock.RedlockAbortSignal,
+ signal: redlockModule.RedlockAbortSignal,
controller: AbortController,
scheduleNext: () => void
): Promise {
diff --git a/internal-packages/sso/src/index.ts b/internal-packages/sso/src/index.ts
index 0ec6df3adbd..422035b141f 100644
--- a/internal-packages/sso/src/index.ts
+++ b/internal-packages/sso/src/index.ts
@@ -50,7 +50,8 @@ export class LazyController implements SsoController {
}
const moduleName = "@triggerdotdev/plugins/sso";
const importer =
- options?.importer ?? ((m: string) => import(m) as Promise<{ default: SsoPlugin }>);
+ options?.importer ??
+ ((m: string) => import(/* @vite-ignore */ m) as Promise<{ default: SsoPlugin }>);
try {
const module = await importer(moduleName);
const plugin: SsoPlugin = module.default;
diff --git a/packages/trigger-sdk/src/v3/aiAutoTelemetry.ts b/packages/trigger-sdk/src/v3/aiAutoTelemetry.ts
index 7533dc9bf05..04bf4b9525a 100644
--- a/packages/trigger-sdk/src/v3/aiAutoTelemetry.ts
+++ b/packages/trigger-sdk/src/v3/aiAutoTelemetry.ts
@@ -42,10 +42,10 @@ async function register(): Promise {
if (typeof aiMod.registerTelemetry !== "function") {
return; // v5 / v6 — `ai` core emits spans itself, nothing to wire.
}
- // Computed specifier keeps the optional peer out of static bundler
- // resolution; resolves at runtime only when the customer installed it.
+ // Computed specifier + @vite-ignore keep the optional peer out of static
+ // bundler resolution; resolves at runtime only when installed.
const otelSpecifier = ["@ai-sdk", "otel"].join("/");
- const otelMod: any = await import(otelSpecifier).catch(() => null);
+ const otelMod: any = await import(/* @vite-ignore */ otelSpecifier).catch(() => null);
if (typeof otelMod?.OpenTelemetry !== "function") {
return; // optional peer not installed
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 72d5a7afc2c..8994da70c9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -548,6 +548,9 @@ importers:
ai:
specifier: 6.0.116
version: 6.0.116(zod@3.25.76)
+ assert:
+ specifier: ^2.1.0
+ version: 2.1.0
assert-never:
specifier: ^1.2.1
version: 1.2.1
@@ -677,6 +680,9 @@ importers:
parse-duration:
specifier: ^2.1.0
version: 2.1.4
+ pg:
+ specifier: 8.15.6
+ version: 8.15.6
posthog-js:
specifier: ^1.93.3
version: 1.93.3
@@ -818,6 +824,9 @@ importers:
ulidx:
specifier: ^2.2.1
version: 2.2.1
+ util:
+ specifier: ^0.12.5
+ version: 0.12.5
uuid:
specifier: ^14.0.0
version: 14.0.0
@@ -993,9 +1002,12 @@ importers:
tsx:
specifier: ^4.20.6
version: 4.20.6
+ vite:
+ specifier: ^6.4.2
+ version: 6.4.2(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0)
vite-tsconfig-paths:
- specifier: ^4.0.5
- version: 4.0.5(typescript@5.5.4)
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.5.4)(vite@6.4.2(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0))
docs: {}
@@ -9171,6 +9183,9 @@ packages:
assert-never@1.2.1:
resolution: {integrity: sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==}
+ assert@2.1.0:
+ resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -9466,10 +9481,6 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
- call-bind@1.0.7:
- resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
- engines: {node: '>= 0.4'}
-
call-bind@1.0.8:
resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
engines: {node: '>= 0.4'}
@@ -10268,10 +10279,6 @@ packages:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
- define-properties@1.1.4:
- resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
- engines: {node: '>= 0.4'}
-
define-properties@1.2.1:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
@@ -11845,6 +11852,10 @@ packages:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
+ is-nan@1.3.2:
+ resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
+ engines: {node: '>= 0.4'}
+
is-negative-zero@2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'}
@@ -13227,6 +13238,10 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
+ object-is@1.1.6:
+ resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
+ engines: {node: '>= 0.4'}
+
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -13583,9 +13598,6 @@ packages:
pg-cloudflare@1.2.7:
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
- pg-connection-string@2.8.5:
- resolution: {integrity: sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==}
-
pg-connection-string@2.9.1:
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
@@ -13602,11 +13614,6 @@ packages:
peerDependencies:
pg: '>=8.0'
- pg-pool@3.9.6:
- resolution: {integrity: sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==}
- peerDependencies:
- pg: '>=8.0'
-
pg-protocol@1.10.3:
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
@@ -13624,15 +13631,6 @@ packages:
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
engines: {node: '>=10'}
- pg@8.11.5:
- resolution: {integrity: sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==}
- engines: {node: '>= 8.0.0'}
- peerDependencies:
- pg-native: '>=3.0.1'
- peerDependenciesMeta:
- pg-native:
- optional: true
-
pg@8.15.6:
resolution: {integrity: sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==}
engines: {node: '>= 8.0.0'}
@@ -15985,6 +15983,14 @@ packages:
vite-tsconfig-paths@4.0.5:
resolution: {integrity: sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==}
+ vite-tsconfig-paths@5.1.4:
+ resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
+ peerDependencies:
+ vite: ^6.4.2
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
vite@4.4.9:
resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -19055,7 +19061,7 @@ snapshots:
'@electric-sql/client@0.4.0':
optionalDependencies:
- '@rollup/rollup-darwin-arm64': 4.53.2
+ '@rollup/rollup-darwin-arm64': 4.60.1
'@electric-sql/client@1.0.14':
dependencies:
@@ -25602,6 +25608,14 @@ snapshots:
assert-never@1.2.1: {}
+ assert@2.1.0:
+ dependencies:
+ call-bind: 1.0.8
+ is-nan: 1.3.2
+ object-is: 1.1.6
+ object.assign: 4.1.5
+ util: 0.12.5
+
assertion-error@2.0.1: {}
ast-v8-to-istanbul@1.0.2:
@@ -25970,14 +25984,6 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
- call-bind@1.0.7:
- dependencies:
- es-define-property: 1.0.1
- es-errors: 1.3.0
- function-bind: 1.1.2
- get-intrinsic: 1.3.0
- set-function-length: 1.2.2
-
call-bind@1.0.8:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -26780,11 +26786,6 @@ snapshots:
define-lazy-prop@3.0.0: {}
- define-properties@1.1.4:
- dependencies:
- has-property-descriptors: 1.0.2
- object-keys: 1.1.1
-
define-properties@1.2.1:
dependencies:
define-data-property: 1.1.4
@@ -28296,7 +28297,7 @@ snapshots:
cosmiconfig: 8.3.6(typescript@5.5.4)
graphile-config: 0.0.1-beta.8
json5: 2.2.3
- pg: 8.11.5
+ pg: 8.15.6
tslib: 2.6.2
yargs: 17.7.2
transitivePeerDependencies:
@@ -28420,7 +28421,7 @@ snapshots:
hast-util-to-jsx-runtime@2.3.6:
dependencies:
- '@types/estree': 1.0.8
+ '@types/estree': 1.0.9
'@types/hast': 3.0.4
'@types/unist': 3.0.3
comma-separated-tokens: 2.0.3
@@ -28756,6 +28757,11 @@ snapshots:
is-interactive@1.0.0: {}
+ is-nan@1.3.2:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+
is-negative-zero@2.0.2: {}
is-negative-zero@2.0.3: {}
@@ -30438,6 +30444,11 @@ snapshots:
object-inspect@1.13.4: {}
+ object-is@1.1.6:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+
object-keys@1.1.1: {}
object.assign@4.1.5:
@@ -30821,19 +30832,13 @@ snapshots:
pg-cloudflare@1.2.7:
optional: true
- pg-connection-string@2.8.5: {}
-
pg-connection-string@2.9.1: {}
pg-int8@1.0.1: {}
pg-numeric@1.0.2: {}
- pg-pool@3.10.1(pg@8.11.5):
- dependencies:
- pg: 8.11.5
-
- pg-pool@3.9.6(pg@8.15.6):
+ pg-pool@3.10.1(pg@8.15.6):
dependencies:
pg: 8.15.6
@@ -30861,26 +30866,16 @@ snapshots:
postgres-interval: 3.0.0
postgres-range: 1.1.4
- pg@8.11.5:
+ pg@8.15.6:
dependencies:
pg-connection-string: 2.9.1
- pg-pool: 3.10.1(pg@8.11.5)
+ pg-pool: 3.10.1(pg@8.15.6)
pg-protocol: 1.10.3
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.2.7
- pg@8.15.6:
- dependencies:
- pg-connection-string: 2.8.5
- pg-pool: 3.9.6(pg@8.15.6)
- pg-protocol: 1.9.5
- pg-types: 2.2.0
- pgpass: 1.0.5
- optionalDependencies:
- pg-cloudflare: 1.2.7
-
pgpass@1.0.5:
dependencies:
split2: 4.2.0
@@ -32632,8 +32627,8 @@ snapshots:
string.prototype.padend@3.1.4:
dependencies:
- call-bind: 1.0.7
- define-properties: 1.1.4
+ call-bind: 1.0.8
+ define-properties: 1.2.1
es-abstract: 1.21.1
string.prototype.trim@1.2.9:
@@ -33690,10 +33685,21 @@ snapshots:
- supports-color
- typescript
+ vite-tsconfig-paths@5.1.4(typescript@5.5.4)(vite@6.4.2(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0)):
+ dependencies:
+ debug: 4.4.3(supports-color@10.0.0)
+ globrex: 0.1.2
+ tsconfck: 3.1.3(typescript@5.5.4)
+ optionalDependencies:
+ vite: 6.4.2(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
vite@4.4.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.46.1):
dependencies:
esbuild: 0.18.20
- postcss: 8.5.10
+ postcss: 8.5.15
rollup: 3.29.1
optionalDependencies:
'@types/node': 22.20.0
@@ -33706,7 +33712,7 @@ snapshots:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- postcss: 8.5.10
+ postcss: 8.5.15
rollup: 4.60.1
tinyglobby: 0.2.16
optionalDependencies:
@@ -33723,7 +33729,7 @@ snapshots:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- postcss: 8.5.10
+ postcss: 8.5.15
rollup: 4.60.1
tinyglobby: 0.2.16
optionalDependencies:
@@ -33740,7 +33746,7 @@ snapshots:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- postcss: 8.5.10
+ postcss: 8.5.15
rollup: 4.60.1
tinyglobby: 0.2.16
optionalDependencies: