From d30ed3421e887923d291845bf0141776112e7495 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 6 Mar 2026 10:24:43 +0000 Subject: [PATCH 1/3] Logs query use_query_condition_cache=1 (#3186) In theory this will make Log queries faster --- internal-packages/clickhouse/src/taskEvents.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal-packages/clickhouse/src/taskEvents.ts b/internal-packages/clickhouse/src/taskEvents.ts index bcb96f03a..cfebd0a24 100644 --- a/internal-packages/clickhouse/src/taskEvents.ts +++ b/internal-packages/clickhouse/src/taskEvents.ts @@ -279,6 +279,9 @@ export function getLogsSearchListQueryBuilder(ch: ClickhouseReader) { "attributes_text", "triggered_timestamp", ], + settings: { + use_query_condition_cache: 1, + }, }); } From 6f6523ff78ce2839841f5c60827a87aede2728dc Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Mar 2026 14:07:32 +0000 Subject: [PATCH 2/3] chore(repo): remove unnecessary "trigger.dev v4.4.2" header from the release PR description (#3183) --- scripts/enhance-release-pr.mjs | 61 +++++++++------------------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/scripts/enhance-release-pr.mjs b/scripts/enhance-release-pr.mjs index 9621d2920..6a446e794 100644 --- a/scripts/enhance-release-pr.mjs +++ b/scripts/enhance-release-pr.mjs @@ -100,13 +100,7 @@ function gitExec(args) { async function getCommitForFile(filePath) { try { // Find the commit that added this file - const sha = await gitExec([ - "log", - "--diff-filter=A", - "--format=%H", - "--", - filePath, - ]); + const sha = await gitExec(["log", "--diff-filter=A", "--format=%H", "--", filePath]); return sha.split("\n")[0] || null; } catch { return null; @@ -118,15 +112,12 @@ async function getPrForCommit(commitSha) { if (!token || !commitSha) return null; try { - const res = await fetch( - `https://api.github.com/repos/${REPO}/commits/${commitSha}/pulls`, - { - headers: { - Authorization: `token ${token}`, - Accept: "application/vnd.github.v3+json", - }, - } - ); + const res = await fetch(`https://api.github.com/repos/${REPO}/commits/${commitSha}/pulls`, { + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json", + }, + }); if (!res.ok) return null; const pulls = await res.json(); @@ -173,9 +164,7 @@ async function parseServerChanges() { } // Look up commits for all files in parallel - const commits = await Promise.all( - fileData.map((f) => getCommitForFile(f.filePath)) - ); + const commits = await Promise.all(fileData.map((f) => getCommitForFile(f.filePath))); // Look up PRs for all commits in parallel const prNumbers = await Promise.all(commits.map((sha) => getPrForCommit(sha))); @@ -222,35 +211,24 @@ function formatPrBody({ version, packageEntries, serverEntries, rawBody }) { const features = packageEntries.filter((e) => e.type === "feature"); const fixes = packageEntries.filter((e) => e.type === "fix"); - const improvements = packageEntries.filter( - (e) => e.type === "improvement" || e.type === "other" - ); + const improvements = packageEntries.filter((e) => e.type === "improvement" || e.type === "other"); const breaking = packageEntries.filter((e) => e.type === "breaking"); const serverFeatures = serverEntries.filter((e) => e.type === "feature"); const serverFixes = serverEntries.filter((e) => e.type === "fix"); - const serverImprovements = serverEntries.filter( - (e) => e.type === "improvement" - ); + const serverImprovements = serverEntries.filter((e) => e.type === "improvement"); const serverBreaking = serverEntries.filter((e) => e.type === "breaking"); const totalFeatures = features.length + serverFeatures.length; const totalFixes = fixes.length + serverFixes.length; const totalImprovements = improvements.length + serverImprovements.length; - lines.push(`# trigger.dev v${version}`); - lines.push(""); - // Summary line const parts = []; - if (totalFeatures > 0) - parts.push(`${totalFeatures} new feature${totalFeatures > 1 ? "s" : ""}`); + if (totalFeatures > 0) parts.push(`${totalFeatures} new feature${totalFeatures > 1 ? "s" : ""}`); if (totalImprovements > 0) - parts.push( - `${totalImprovements} improvement${totalImprovements > 1 ? "s" : ""}` - ); - if (totalFixes > 0) - parts.push(`${totalFixes} bug fix${totalFixes > 1 ? "es" : ""}`); + parts.push(`${totalImprovements} improvement${totalImprovements > 1 ? "s" : ""}`); + if (totalFixes > 0) parts.push(`${totalFixes} bug fix${totalFixes > 1 ? "es" : ""}`); if (parts.length > 0) { lines.push(`## Summary`); lines.push(`${parts.join(", ")}.`); @@ -260,8 +238,7 @@ function formatPrBody({ version, packageEntries, serverEntries, rawBody }) { // Breaking changes if (breaking.length > 0 || serverBreaking.length > 0) { lines.push("## Breaking changes"); - for (const entry of [...breaking, ...serverBreaking]) - lines.push(`- ${entry.text}`); + for (const entry of [...breaking, ...serverBreaking]) lines.push(`- ${entry.text}`); lines.push(""); } @@ -290,17 +267,11 @@ function formatPrBody({ version, packageEntries, serverEntries, rawBody }) { } // Server changes - const allServer = [ - ...serverFeatures, - ...serverImprovements, - ...serverFixes, - ]; + const allServer = [...serverFeatures, ...serverImprovements, ...serverFixes]; if (allServer.length > 0) { lines.push("## Server changes"); lines.push(""); - lines.push( - "These changes affect the self-hosted Docker image and Trigger.dev Cloud:" - ); + lines.push("These changes affect the self-hosted Docker image and Trigger.dev Cloud:"); lines.push(""); for (const entry of allServer) { // Indent continuation lines so multi-line entries stay inside the list item From e64b101138d0428c9de88e1819901022e6bb89dc Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Mar 2026 15:31:16 +0000 Subject: [PATCH 3/3] feat(webapp): Add test payload AI generation to the test page based on payload schemas (#3188) CleanShot 2026-03-06 at 13 36 53 CleanShot 2026-03-06 at 13 37 28 Add a tabbed sidebar to the Test page for standard tasks, reusing the ClientTabs pattern from the Query page. - Options tab: existing sidebar content (machine, version, queue, etc.) - AI tab: AI-powered payload generation with streaming, supports JSON Schema, inferred schema from recent runs, and task source code lookup via tool calling for tasks without schemas - Schema tab: displays payload JSON Schema (from schemaTask), inferred schema (from recent runs via @jsonhero/schema-infer), or empty state with schemaTask docs and example code Data layer changes: - Surface payloadSchema and inferredPayloadSchema from TestTaskPresenter - Add payloadSchema and fileId to WorkerDeploymentWithWorkerTasks type - Decompress zlib-deflated source files for AI context --- .server-changes/test-page-sidebar-tabs.md | 6 + .../presenters/v3/TestTaskPresenter.server.ts | 22 + .../AIPayloadTabContent.tsx | 377 ++++++++++++ .../SchemaTabContent.tsx | 93 +++ .../TestSidebarTabs.tsx | 78 +++ .../route.tsx | 538 ++++++++++-------- ...env.$envParam.test.ai-generate-payload.tsx | 312 ++++++++++ .../app/v3/models/workerDeployment.server.ts | 2 + apps/webapp/package.json | 1 + pnpm-lock.yaml | 60 ++ 10 files changed, 1236 insertions(+), 253 deletions(-) create mode 100644 .server-changes/test-page-sidebar-tabs.md create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/SchemaTabContent.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/TestSidebarTabs.tsx create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.ai-generate-payload.tsx diff --git a/.server-changes/test-page-sidebar-tabs.md b/.server-changes/test-page-sidebar-tabs.md new file mode 100644 index 000000000..1803f2c2a --- /dev/null +++ b/.server-changes/test-page-sidebar-tabs.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: feature +--- + +Add sidebar tabs (Options, AI, Schema) to the Test page for schemaTask payload generation and schema viewing. diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 2817b7c8b..09abb2263 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -6,6 +6,7 @@ import { type TaskRunTemplate, PrismaClientOrTransaction, } from "@trigger.dev/database"; +import { inferSchema } from "@jsonhero/schema-infer"; import parse from "parse-duration"; import { type PrismaClient } from "~/db.server"; import { RunsRepository } from "~/services/runsRepository/runsRepository.server"; @@ -34,6 +35,8 @@ type Task = { taskIdentifier: string; filePath: string; friendlyId: string; + payloadSchema?: unknown; + inferredPayloadSchema?: unknown; }; type Queue = { @@ -244,11 +247,30 @@ export class TestTaskPresenter { }, }); + // Infer schema from existing run payloads when no explicit schema is defined + let inferredPayloadSchema: unknown | undefined; + if (!task.payloadSchema && latestRuns.length > 0 && task.triggerSource === "STANDARD") { + let inference: ReturnType | undefined; + for (const run of latestRuns) { + try { + const parsed = await parsePacket({ data: run.payload, dataType: run.payloadType }); + inference = inferSchema(parsed, inference); + } catch { + // Skip malformed runs — inference is best-effort + } + } + if (inference) { + inferredPayloadSchema = inference.toJSONSchema(); + } + } + const taskWithEnvironment = { id: task.id, taskIdentifier: task.slug, filePath: task.filePath, friendlyId: task.friendlyId, + payloadSchema: task.payloadSchema ?? undefined, + inferredPayloadSchema, }; switch (task.triggerSource) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx new file mode 100644 index 000000000..3d9302356 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx @@ -0,0 +1,377 @@ +import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { AnimatePresence, motion } from "framer-motion"; +import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"; +import { SparkleListIcon } from "~/assets/icons/SparkleListIcon"; +import { Button } from "~/components/primitives/Buttons"; +import { Header3 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { Spinner } from "~/components/primitives/Spinner"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { cn } from "~/utils/cn"; + +const StreamdownRenderer = lazy(() => + import("streamdown").then((mod) => ({ + default: ({ children, isAnimating }: { children: string; isAnimating: boolean }) => ( + + {children} + + ), + })) +); + +type StreamEventType = + | { type: "thinking"; content: string } + | { type: "result"; success: true; payload: string } + | { type: "result"; success: false; error: string }; + +export function AIPayloadTabContent({ + onPayloadGenerated, + payloadSchema, + taskIdentifier, + getCurrentPayload, +}: { + onPayloadGenerated: (payload: string) => void; + payloadSchema?: unknown; + taskIdentifier: string; + getCurrentPayload?: () => string; +}) { + const [prompt, setPrompt] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const isLoadingRef = useRef(false); + const [thinking, setThinking] = useState(""); + const [error, setError] = useState(null); + const [showThinking, setShowThinking] = useState(false); + const [lastResult, setLastResult] = useState<"success" | "error" | null>(null); + const textareaRef = useRef(null); + const abortControllerRef = useRef(null); + + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + const resourcePath = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/test/ai-generate-payload`; + + const submitGeneration = useCallback( + async (queryPrompt: string) => { + if (!queryPrompt.trim() || isLoadingRef.current) return; + + isLoadingRef.current = true; + setIsLoading(true); + setThinking(""); + setError(null); + setShowThinking(true); + setLastResult(null); + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + + try { + const formData = new FormData(); + formData.append("prompt", queryPrompt); + formData.append("taskIdentifier", taskIdentifier); + if (payloadSchema) { + formData.append("payloadSchema", JSON.stringify(payloadSchema)); + } + const currentPayload = getCurrentPayload?.(); + if (currentPayload) { + formData.append("currentPayload", currentPayload); + } + + const response = await fetch(resourcePath, { + method: "POST", + body: formData, + signal: abortControllerRef.current.signal, + }); + + if (!response.ok) { + const errorData = (await response.json()) as { error?: string }; + setError(errorData.error || "Failed to generate payload"); + setIsLoading(false); + setLastResult("error"); + return; + } + + const reader = response.body?.getReader(); + if (!reader) { + setError("No response stream"); + setIsLoading(false); + setLastResult("error"); + return; + } + + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + const lines = buffer.split("\n\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.startsWith("data: ")) { + try { + const event = JSON.parse(line.slice(6)) as StreamEventType; + processStreamEvent(event); + } catch { + // Ignore parse errors + } + } + } + } + + if (buffer.startsWith("data: ")) { + try { + const event = JSON.parse(buffer.slice(6)) as StreamEventType; + processStreamEvent(event); + } catch { + // Ignore parse errors + } + } + } catch (err) { + if (err instanceof Error && err.name === "AbortError") return; + setError(err instanceof Error ? err.message : "An error occurred"); + setLastResult("error"); + } finally { + isLoadingRef.current = false; + setIsLoading(false); + } + }, + [resourcePath, taskIdentifier, payloadSchema, getCurrentPayload] + ); + + const processStreamEvent = useCallback( + (event: StreamEventType) => { + switch (event.type) { + case "thinking": + setThinking((prev) => prev + event.content); + break; + case "result": + if (event.success) { + onPayloadGenerated(event.payload); + setPrompt(""); + setLastResult("success"); + } else { + setError(event.error); + setLastResult("error"); + } + break; + } + }, + [onPayloadGenerated] + ); + + const handleSubmit = useCallback( + (e?: React.FormEvent) => { + e?.preventDefault(); + submitGeneration(prompt); + }, + [prompt, submitGeneration] + ); + + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + + useEffect(() => { + if (error) { + const timer = setTimeout(() => setError(null), 15000); + return () => clearTimeout(timer); + } + }, [error]); + + const examplePrompts = payloadSchema + ? [ + "Generate a valid payload", + "Generate a payload with edge cases", + "Generate a minimal payload with only required fields", + ] + : [ + "Generate a simple JSON payload", + "Generate a payload with nested objects", + "Generate a payload with an array of items", + ]; + + return ( +
+
+
+
+