feat: AI prompt management dashboard and enhanced span inspectors#3244
feat: AI prompt management dashboard and enhanced span inspectors#3244
Conversation
…rride fixes, llm pricing sync
…s filter, show version in generation rows
…rsion/override display, breadcrumb fix
…, streamText, generateObject, toolCall, embed)
|
WalkthroughAdds end-to-end prompt management: database migrations and Prisma models for prompts and prompt_versions; ClickHouse schema and ingestion fields (prompt_slug, prompt_version); server-side presenter and service implementations (PromptPresenter, PromptService) plus multiple authenticated API routes for listing, resolving, promoting, and managing overrides; UI pages/components (prompts list, prompt detail, filters, dashboard integration, inspector views); SDK/CLI/catalog support to declare and ship prompts (definePrompt, resource-catalog, MCP tools); worker indexing, background prompt upsert logic, and event enrichment with prompt telemetry. Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 12
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
internal-packages/llm-pricing/src/default-model-prices.json (1)
3967-3971:⚠️ Potential issue | 🟠 MajorWiden the live Gemini prefix too.
This is the only
gemini*entry in the catalog that still rejectsgoogleai/...— all other 16 Gemini models use the widenedgoogle(ai)?/pattern. If callers resolve the live model with that prefix, pricing will miss. Update the pattern and re-runpnpm run sync-prices.Suggested fix
- "matchPattern": "(?i)^(google\/)?(gemini-live-2.5-flash-native-audio)$", + "matchPattern": "(?i)^(google(ai)?\/)?(gemini-live-2.5-flash-native-audio)$",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal-packages/llm-pricing/src/default-model-prices.json` around lines 3967 - 3971, Update the matchPattern for the entry with id "029e6695-ff24-47f0-b37b-7285fb2e5785" (modelName "gemini-live-2.5-flash-native-audio") to accept the widened live Gemini prefix used by other Gemini entries: replace the current "(?i)^(google\/)?(gemini-live-2.5-flash-native-audio)$" pattern with one that allows both "google/" and "googleai/" prefixes (matching the other Gemini entries), then re-run "pnpm run sync-prices" to propagate the change.apps/webapp/app/components/metrics/QueryWidget.tsx (1)
452-458:⚠️ Potential issue | 🟠 MajorFullscreen table ignores hidden column configuration.
Line 452-458 renders
TSQLResultsTablewithouthiddenColumns, so hidden fields reappear when maximized.Proposed fix
<TSQLResultsTable rows={data.rows} columns={data.columns} prettyFormatting={config.prettyFormatting} sorting={config.sorting} showHeaderOnEmpty={showTableHeaderOnEmpty} + hiddenColumns={hiddenColumns} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/metrics/QueryWidget.tsx` around lines 452 - 458, The fullscreen TSQLResultsTable render is missing the hiddenColumns prop so columns configured as hidden reappear when maximized; update the TSQLResultsTable usage in QueryWidget (the block rendering TSQLResultsTable) to pass the hiddenColumns from the current config (e.g., hiddenColumns={config.hiddenColumns}) so the table respects hidden field configuration in both normal and fullscreen modes.
🟡 Minor comments (10)
apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx-74-79 (1)
74-79:⚠️ Potential issue | 🟡 MinorFix minute/second formatting to avoid
60soutputs.At Line 78,
.toFixed(0)can round up and produce invalid displays like1m 60s. Use floor/truncation for seconds.Proposed fix
function formatDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; const mins = Math.floor(ms / 60_000); - const secs = ((ms % 60_000) / 1000).toFixed(0); + const secs = Math.floor((ms % 60_000) / 1000); return `${mins}m ${secs}s`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx` around lines 74 - 79, The formatDuration function can produce "1m 60s" because secs is computed with toFixed(0) which can round up; update formatDuration to compute seconds with truncation/flooring instead (e.g., secs = Math.floor((ms % 60_000) / 1000)) so seconds never equal 60, and return the string using that secs value; change occurs in the formatDuration function where secs is currently set with .toFixed(0).internal-packages/clickhouse/src/llmMetrics.ts-41-41 (1)
41-41:⚠️ Potential issue | 🟡 MinorConstrain
prompt_versionto an integer.
prompt_versioncurrently accepts any number; allowing decimals here can create invalid prompt-version semantics in stored metrics.Proposed fix
- prompt_version: z.number(), + prompt_version: z.number().int().nonnegative(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal-packages/clickhouse/src/llmMetrics.ts` at line 41, The prompt_version field in the Zod schema is currently z.number(), which allows non-integer values; update the prompt_version schema to only accept integers (e.g., replace z.number() with z.number().int() or add a refine that checks Number.isInteger) so stored metrics can't have decimal prompt versions; change the prompt_version entry in the llmMetrics schema accordingly.apps/webapp/app/components/code/TSQLResultsTable.tsx-1049-1052 (1)
1049-1052:⚠️ Potential issue | 🟡 MinorHandle “all columns hidden” using
visibleColumnsto avoid blank rendering.After introducing
hiddenColumns, table guards still usecolumns.length. If all columns are hidden, the component can render an unusable empty structure.💡 Suggested fix
- if (!columns.length) return null; + if (!visibleColumns.length) { + return <ChartBlankState icon={IconTable} message="No columns to display" />; + } ... - <td className="w-full px-3 py-6" colSpan={columns.length}> + <td className="w-full px-3 py-6" colSpan={visibleColumns.length}>Also applies to: 1106-1106, 1153-1153
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/code/TSQLResultsTable.tsx` around lines 1049 - 1052, The component's rendering guards still check columns.length instead of the computed visibleColumns, so if all columns are hidden the table renders empty; update every guard and conditional that currently uses columns.length to use visibleColumns.length (the useMemo result) and add an explicit early return or fallback UI when visibleColumns.length === 0; search for occurrences around where visibleColumns is used (e.g., in TSQLResultsTable render/guard logic and the other two spots noted) and replace those checks so the component does not render an empty table when all columns are hidden.apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx-63-66 (1)
63-66:⚠️ Potential issue | 🟡 MinorKeep telemetry
promptfallback when structured prompt fields are missing.Filtering out
promptunconditionally can hide prompt data for older spans that only populatetelemetryMetadata.prompt.💡 Suggested fix
- {aiData.telemetryMetadata && - Object.entries(aiData.telemetryMetadata) - .filter(([key]) => key !== "prompt") - .map(([key, value]) => <MetricRow key={key} label={key} value={value} />)} + {aiData.telemetryMetadata && + Object.entries(aiData.telemetryMetadata) + .filter(([key]) => !(key === "prompt" && aiData.promptSlug)) + .map(([key, value]) => <MetricRow key={key} label={key} value={value} />)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx` around lines 63 - 66, Currently the code unconditionally filters out the "prompt" key from aiData.telemetryMetadata, which removes prompt fallback for older spans; modify the logic so "prompt" is only excluded when structured prompt fields exist—e.g., compute hasStructuredPrompt = Object.keys(aiData.telemetryMetadata).some(k => k !== "prompt" && k.startsWith("prompt")) (or check for known structured keys) and then only filter out "prompt" when hasStructuredPrompt is true; keep the mapping to MetricRow unchanged so MetricRow still receives telemetry entries including the fallback "prompt" when no structured prompt fields are present.apps/webapp/app/routes/api.v1.prompts.$slug.override.ts-27-31 (1)
27-31:⚠️ Potential issue | 🟡 MinorKeep the 405 responses aligned with the actual method set.
The loader's non-
OPTIONSpath drops the CORS wrapper andAllowheader entirely, and the action advertisesPOST, PUT, DELETEeven thoughPATCHis accepted above. Unsupported-method clients will get misleading metadata.Also applies to: 113-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/routes/api.v1.prompts`.$slug.override.ts around lines 27 - 31, The loader's non-OPTIONS branch returns a bare 405 without CORS or an accurate Allow header; update loader (export async function loader) to wrap the 405 response with apiCors and include an Allow header that matches the actual supported methods (include PATCH alongside POST, PUT, DELETE as accepted elsewhere), and likewise fix the action's 405 paths (the branch around lines ~113-116) so they also use apiCors and the same correct Allow header to avoid misleading metadata.apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx-38-43 (1)
38-43:⚠️ Potential issue | 🟡 MinorReset the active tab when the available sections change.
tabpersists across prop changes. If someone is on "Input" or "Template" and then opens a prompt span without that section, the header renders with no active tab and the body stays blank until they click "Overview".Suggested fix
-import { lazy, Suspense, useState } from "react"; +import { lazy, Suspense, useEffect, useState } from "react"; @@ const [tab, setTab] = useState<PromptTab>("overview"); + + useEffect(() => { + if ((tab === "input" && !hasInput) || (tab === "template" && !hasTemplate)) { + setTab("overview"); + } + }, [tab, hasInput, hasTemplate]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx` around lines 38 - 43, The active tab stored in state (tab via useState and setTab) can become invalid when availableTabs changes; add a useEffect in the PromptSpanDetails component that watches availableTabs and, if the current tab is not included in availableTabs, calls setTab(availableTabs[0]) (or another sane default) to reset to a valid tab; ensure the effect lists availableTabs (or a stable derived key) in its dependency array so the active tab updates whenever availableTabs changes.packages/cli-v3/src/mcp/tools/prompts.ts-50-61 (1)
50-61:⚠️ Potential issue | 🟡 MinorAdd error handling for non-JSON error responses.
If the API returns a non-JSON error response (e.g., 500 with HTML),
res.json()will throw before the error message can be extracted. Consider handling this case.🛡️ Proposed fix for safer error handling
async function fetchPromptApi( apiClient: { fetchClient: typeof fetch; baseUrl: string }, path: string, options?: RequestInit ) { const res = await apiClient.fetchClient(`${apiClient.baseUrl}/api/v1/prompts${path}`, options); - const data = await res.json(); if (!res.ok) { - throw new Error(data.error ?? `API error ${res.status}`); + let errorMessage = `API error ${res.status}`; + try { + const data = await res.json(); + if (data.error) errorMessage = data.error; + } catch { + // Response wasn't JSON, use status code message + } + throw new Error(errorMessage); } - return data; + return res.json(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli-v3/src/mcp/tools/prompts.ts` around lines 50 - 61, The fetchPromptApi function currently calls res.json() unconditionally which will throw on non-JSON responses; update fetchPromptApi to first read the response body safely (e.g., await res.text()), then if res.ok try to parse JSON (JSON.parse) and return it, and if !res.ok attempt to parse the text as JSON to extract data.error but fall back to including the raw text and status in the thrown Error; reference the existing fetchPromptApi function and apiClient.fetchClient call and replace the direct res.json() usage with the safe text-then-JSON parsing/fallback logic so non-JSON error responses (HTML, plain text) are surfaced in the error message.apps/webapp/app/presenters/v3/SpanPresenter.server.ts-841-849 (1)
841-849:⚠️ Potential issue | 🟡 MinorHandle potential NaN from version parsing.
Number(promptVersion)on a non-numeric string returnsNaN, which would cause the Prisma query to fail or return unexpected results. Consider adding validation.🛡️ Proposed fix to validate version number
const version = await this._replica.promptVersion.findUnique({ where: { promptId_version: { promptId: prompt.id, - version: Number(promptVersion), + version: parseInt(promptVersion, 10), }, }, }); - if (!version) return undefined; + if (!version || isNaN(version.version)) return undefined;Alternatively, validate before the query:
+ const versionNum = parseInt(promptVersion, 10); + if (isNaN(versionNum)) return undefined; + const version = await this._replica.promptVersion.findUnique({ where: { promptId_version: { promptId: prompt.id, - version: Number(promptVersion), + version: versionNum, }, }, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/presenters/v3/SpanPresenter.server.ts` around lines 841 - 849, The code calls this._replica.promptVersion.findUnique using Number(promptVersion) which can be NaN; before querying, parse and validate promptVersion (e.g., with Number.isFinite / Number.isInteger or parseInt) and handle invalid values by returning undefined or throwing as appropriate; update the logic around the version lookup (the promptVersion input, the Number(...) conversion, and the variable version) so the query only runs with a valid numeric version (and include prompt.id in the check) to avoid passing NaN into Prisma.apps/webapp/app/v3/services/promptService.server.ts-115-116 (1)
115-116:⚠️ Potential issue | 🟡 MinorSame race condition applies to
reactivateOverride.Consider wrapping the
#removeLabeland#addLabelcalls in a transaction here as well for consistency withcreateOverride.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/v3/services/promptService.server.ts` around lines 115 - 116, reactivateOverride currently calls this.#removeLabel(promptId, "override") and this.#addLabel(versionId, "override") separately, causing the same race condition as createOverride; modify reactivateOverride to run both label operations inside a single database transaction (use the same transaction helper or mechanism used by createOverride) so that the remove/add occur atomically, pass the transaction/context into `#removeLabel` and `#addLabel` (or call transactional variants), and ensure errors roll back the transaction and propagate appropriately.apps/webapp/app/v3/services/promptService.server.ts-21-22 (1)
21-22:⚠️ Potential issue | 🟡 MinorPotential race condition between label operations.
#removeLabeland#addLabelare not wrapped in a transaction, so concurrentpromoteVersioncalls could leave the system in an inconsistent state (e.g., no version with "current" label, or multiple versions with it).🔒 Suggested fix: wrap in transaction
- await this.#removeLabel(promptId, "current"); - await this.#addLabel(versionId, "current"); + await this._prisma.$transaction(async (tx) => { + await tx.$executeRaw` + UPDATE "prompt_versions" + SET "labels" = array_remove("labels", 'current') + WHERE "promptId" = ${promptId} AND 'current' = ANY("labels") + `; + await tx.$executeRaw` + UPDATE "prompt_versions" + SET "labels" = array_append("labels", 'current') + WHERE "id" = ${versionId} AND NOT ('current' = ANY("labels")) + `; + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/v3/services/promptService.server.ts` around lines 21 - 22, The two label operations (`#removeLabel` and `#addLabel`) can race; wrap them in a single database transaction inside the promoteVersion flow so both run atomically on the same DB session/connection: begin a transaction, call `#removeLabel`(promptId, "current") then `#addLabel`(versionId, "current") using the transactional context, commit on success and rollback on error, and ensure both helper methods accept/propagate the transaction/session (or inline their SQL/ORM calls into the transaction) so concurrent promoteVersion calls cannot leave multiple or zero "current" labels.
🧹 Nitpick comments (16)
apps/webapp/app/components/code/TextEditor.tsx (3)
12-19: Use a type alias forTextEditorPropsLine 12 uses an
interface; this repo standard preferstypealiases in TS/TSX.♻️ Suggested change
-export interface TextEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> { +export type TextEditorProps = Omit<ReactCodeMirrorProps, "onBlur"> & { defaultValue?: string; readOnly?: boolean; onChange?: (value: string) => void; onUpdate?: (update: ViewUpdate) => void; showCopyButton?: boolean; additionalActions?: React.ReactNode; -} +};As per coding guidelines,
**/*.{ts,tsx}: Use types over interfaces for TypeScript.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/code/TextEditor.tsx` around lines 12 - 19, Replace the interface declaration with a type alias: change the exported TextEditorProps interface into an exported type alias that equals Omit<ReactCodeMirrorProps, "onBlur"> & { ... } and keep all existing fields (defaultValue, readOnly, onChange, onUpdate, showCopyButton, additionalActions) unchanged; update any imports/uses still referring to TextEditorProps if needed but the symbol name stays the same (TextEditorProps) and the file TextEditor.tsx should reflect the type alias form.
1-125: Add@crumbstracing markers in this new TSX componentThis file introduces new behavior but has no
@crumbsmarkers. Please add either inline//@Crumbscomments or a `// `#region` `@crumbsblock per repo convention.As per coding guidelines,
**/*.{ts,tsx,js}: Add crumbs as you write code using //@crumbscomments or //#region@crumbsblocks for agentcrumbs debug tracing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/code/TextEditor.tsx` around lines 1 - 125, Add the required `@crumbs` tracing markers to this new TextEditor component by inserting either inline // `@crumbs` comments or a // `#region` `@crumbs` block around the component entry points: place a top-level marker just above the TextEditor function declaration and add markers at key internal points such as inside the useEffect that sets the container (useRef editor/current), the useEffect that syncs defaultValue to view, and the copy callback (copy) to trace user actions and lifecycle; ensure markers reference the function names (TextEditor, copy) and the view/setContainer usage so they are easy to find.
33-46: Use declarative array construction instead of mutationLines 33-46 build
extensionsvia sequentialpush()calls. WhilegetEditorSetup(false)returns a fresh array each invocation (not shared), imperative mutation is less readable and maintainable. Create the array declaratively using the spread operator.♻️ Suggested change
-import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ - const extensions = getEditorSetup(false); - extensions.push(EditorView.lineWrapping); - extensions.push( - lineNumbers({ - formatNumber: (n) => String(n), - }) - ); - extensions.push( - EditorView.theme({ - ".cm-lineNumbers": { - minWidth: "40px", - }, - }) - ); + const extensions = useMemo( + () => [ + ...getEditorSetup(false), + EditorView.lineWrapping, + lineNumbers({ + formatNumber: (n) => String(n), + }), + EditorView.theme({ + ".cm-lineNumbers": { + minWidth: "40px", + }, + }), + ], + [] + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/code/TextEditor.tsx` around lines 33 - 46, Replace the imperative pushes that mutate the extensions array with a declarative array construction: create a new array by spreading the result of getEditorSetup(false) and appending EditorView.lineWrapping, the lineNumbers(...) call (with formatNumber), and the EditorView.theme(...) object (with ".cm-lineNumbers" minWidth). Update the variable named extensions to be initialized with that single array expression so the code uses a readable immutable pattern around getEditorSetup, EditorView.lineWrapping, lineNumbers, and EditorView.theme.packages/cli-v3/src/entryPoints/managed-index-worker.ts (1)
205-220: Consider consolidating prompt/task schema conversion into one shared helper.The two conversion paths now duplicate the same error-handling and mapping pattern; a generic helper would reduce drift risk.
Also applies to: 222-239
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli-v3/src/entryPoints/managed-index-worker.ts` around lines 205 - 220, The convertPromptSchemasToJsonSchemas function duplicates the mapping + try/catch pattern used elsewhere (e.g., the similar task conversion block around lines 222-239); extract a shared helper (e.g., mapWithSchemaConversion) that accepts the items array and a schema lookup function (resourceCatalog.getPromptSchema / getTaskSchema) and performs the schemaToJsonSchema conversion with the same safe try/catch logic, then replace convertPromptSchemasToJsonSchemas and the task conversion with calls to that helper to avoid duplicated error handling and mapping logic.apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx (1)
98-100: Unreachable code: empty state already handled earlier.The
TableBlankRowon lines 98-100 will never render becauseprompts.length === 0is already handled with an early return at line 64. This ternary can be simplified to just map the prompts directly.♻️ Suggested simplification
<TableBody> - {prompts.length === 0 ? ( - <TableBlankRow colSpan={7}>No prompts found</TableBlankRow> - ) : ( - prompts.map((prompt) => { + {prompts.map((prompt) => { const path = `${v3PromptsPath(organization, project, environment)}/${prompt.slug}`; const activeVersion = prompt.overrideVersion ?? prompt.currentVersion; const isOverride = !!prompt.overrideVersion; // ... rest of mapping - }) - )} + })} </TableBody>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx around lines 98 - 100, The ternary that renders TableBlankRow when prompts.length === 0 is unreachable because an early return handles the empty state; remove the conditional branch and simplify the JSX to directly map over prompts (use the existing prompts array mapping logic) and delete the unreachable TableBlankRow usage; update the render in the route component where prompts is referenced and remove any dead code path that checks prompts.length === 0 (symbols to locate: prompts, TableBlankRow).apps/webapp/app/v3/services/createBackgroundWorker.server.ts (1)
710-711: Move import to the top of the file.The
import { createHash } from "crypto"statement is placed at the end of the file (line 711), which is unconventional and may cause confusion. Node.js built-in imports should be grouped with other imports at the top.♻️ Move import to top
Add at the top with other imports (around line 1-9):
import { createHash } from "crypto";Then remove lines 710-711.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/v3/services/createBackgroundWorker.server.ts` around lines 710 - 711, The file currently contains a stray "import { createHash } from 'crypto'" near the end; move that import up into the existing import block at the top of the file with the other imports (so createHash is imported alongside the top-level imports) and remove the trailing import statement at the bottom (the one currently on lines 710-711) to keep imports grouped and conventional.apps/webapp/app/components/metrics/ProvidersFilter.tsx (1)
18-20: Prefer a type alias for the props shape.This repo standardizes on
typeinstead ofinterfacein TypeScript files. As per coding guidelines, "Use types over interfaces for TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/metrics/ProvidersFilter.tsx` around lines 18 - 20, Replace the interface declaration for ProvidersFilterProps with a type alias to follow project conventions: change the declaration "interface ProvidersFilterProps { possibleProviders: string[] }" to a type alias named ProvidersFilterProps with the same shape (possibleProviders: string[]), ensuring any references to ProvidersFilterProps (e.g., in the ProvidersFilter component props) continue to work unchanged.apps/webapp/app/components/metrics/OperationsFilter.tsx (1)
18-20: Prefer a type alias for the props shape.This repo standardizes on
typeinstead ofinterfacein TypeScript files. As per coding guidelines, "Use types over interfaces for TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/metrics/OperationsFilter.tsx` around lines 18 - 20, Replace the interface declaration for OperationsFilterProps with a type alias to match repository standards: change the "interface OperationsFilterProps { possibleOperations: string[] }" to a "type OperationsFilterProps = { possibleOperations: string[] }" and ensure any usages of OperationsFilterProps (e.g., in the OperationsFilter component props) continue to reference the same symbol.apps/webapp/app/components/metrics/PromptsFilter.tsx (1)
18-20: Prefer a type alias for the props shape.This repo standardizes on
typeinstead ofinterfacein TypeScript files. As per coding guidelines, "Use types over interfaces for TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/metrics/PromptsFilter.tsx` around lines 18 - 20, Replace the interface declaration for PromptsFilterProps with a type alias: change "interface PromptsFilterProps { possiblePrompts: string[] }" to "type PromptsFilterProps = { possiblePrompts: string[] }"; update any usages or exports of PromptsFilterProps as needed (no other logic changes).apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx (1)
296-311: Consider extracting shared helpers to reduce duplication.
PromptMetricRowandtryPrettyJsonare duplicated inAIToolCallSpanDetails.tsx. Consider extracting these to a shared module (e.g.,~/components/runs/v3/ai/shared.tsx) if this pattern continues to expand.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx` around lines 296 - 311, Extract the duplicated PromptMetricRow component and tryPrettyJson function into a shared module (e.g., create a new file like shared.tsx) and replace the local definitions in AISpanDetails.tsx and AIToolCallSpanDetails.tsx with imports from that module; specifically move the PromptMetricRow function and the tryPrettyJson function into the shared file, export them, update both files to import { PromptMetricRow, tryPrettyJson } from the new shared module, and run a quick typecheck to ensure props and return types align.apps/webapp/app/routes/api.v1.prompts.$slug.ts (1)
152-174: Template compiler has limited Mustache compatibility.The
compileTemplatefunction handles basic{{key}}interpolation and simple{{#key}}...{{/key}}blocks, but:
- Nested blocks (
{{#outer}}{{#inner}}...{{/inner}}{{/outer}}) won't work correctly due to greedy regex- Inverted sections (
{{^key}}) are not supported- Comments (
{{! comment }}) are not strippedIf this is intentional for a simplified template syntax, consider documenting the supported syntax. Otherwise, consider using a lightweight template library.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/routes/api.v1.prompts`.$slug.ts around lines 152 - 174, The compileTemplate function currently uses greedy regexes which break nested sections (e.g., {{`#outer`}}{{`#inner`}}...{{/inner}}{{/outer}}), and it does not handle inverted sections ({{^key}}) or strip comments ({{! ... }}); either document these limitations in the function's JSDoc and README as intentional, or replace the implementation with a proper Mustache-compatible renderer: remove the ad-hoc regex logic in compileTemplate and call a lightweight library (e.g., mustache.render or hogan.js) to get correct handling of nested blocks, inverted sections, and comments, or implement a small recursive parser that matches opening/closing tags to support nesting and add support for ^-sections and comment stripping before interpolation.internal-packages/database/prisma/schema.prisma (1)
588-588: Consider documenting expectedsourcevalues.The
sourcefield is a String with comment noting expected values. Consider adding a more detailed comment or using a Prisma@defaultvalue to make the expected values clearer for future maintainers.source String // Expected: "code" | "dashboard" | "api"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal-packages/database/prisma/schema.prisma` at line 588, The schema's source field currently is a plain String (source) with an inline comment; change it to a stronger, self-documenting type or add a clearer default/comment: either define a Prisma enum (e.g., enum Source { deploy dashboard api }) and replace source String with source Source, or add a more explicit inline comment and a sensible `@default` (e.g., `@default`("dashboard")) to schema.prisma so the expected values ("deploy" | "dashboard" | "api") are enforced/visible; update any code that writes/reads this field to use the enum values if you choose the enum route (look for usages of source in migrations, models, and repository functions).packages/trigger-sdk/src/v3/prompt.ts (2)
56-67: Template conditional section regex may not handle nested conditionals.The regex
/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/guses non-greedy matching which works for simple cases, but nested conditionals like{{#a}}{{#b}}...{{/b}}{{/a}}would be incorrectly parsed due to the first{{/being matched with the outer{{#a}}.If nested conditionals are not a supported feature, consider adding a note in the docstring. Otherwise, a recursive or stack-based parser would be needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/trigger-sdk/src/v3/prompt.ts` around lines 56 - 67, The current conditional parser using template.replace with the regex /\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g (assigned to result) does not support nested conditionals (e.g., {{`#a`}}{{`#b`}}...{{/b}}{{/a}}); either explicitly document in the prompt module's function comment that nested conditionals are not supported, or replace the regex-based approach with a simple recursive/stack-based parser: parse the template char-by-char, push opening tags ({{`#key`}}) onto a stack, build node/content trees, and evaluate nodes using variables to correctly handle nested {{#...}}{{/...}} blocks—update the function that currently performs the replace to use this parser and keep existing variable interpolation logic for inner content.
160-208: API resolution lacks explicit error handling.If
apiClient.resolvePromptthrows or returns an error response, the promise rejects without a user-friendly message. Consider wrapping in try/catch to provide context about prompt resolution failures.💡 Suggested improvement
if (ctx && apiClient) { + try { const response = await apiClient.resolvePrompt( options.id, // ... existing code ... ); // ... existing return logic ... + } catch (error) { + throw new Error( + `Failed to resolve prompt "${options.id}": ${error instanceof Error ? error.message : String(error)}` + ); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/trigger-sdk/src/v3/prompt.ts` around lines 160 - 208, Wrap the apiClient.resolvePrompt(...) call inside a try/catch in the same block where ctx and apiClient are checked (the prompt resolution path that currently invokes apiClient.resolvePrompt with tracer/name "prompt.resolve()"); on catch, create a clear, contextual error message that includes the prompt id (options.id) and any relevant resolveOptions (label/version) and either log it via the existing logger/tracer or throw a new Error that wraps the original error so callers get a user-friendly message; ensure you preserve and rethrow the original error as the cause if available so stack traces remain useful.apps/webapp/app/presenters/v3/PromptPresenter.server.ts (2)
6-32: Consider consolidating GenerationRow definitions.The
GenerationRowSchema(zod) andGenerationRow(type) have slightly different field names (started_atvsstart_time). While the transformation at line 334 handles this, having two similar definitions could lead to drift.Consider deriving the type from the schema:
type GenerationRowRaw = z.infer<typeof GenerationRowSchema>;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts` around lines 6 - 32, The GenerationRowSchema and GenerationRow types are duplicated and have a mismatched field name (started_at vs start_time); derive a single source of truth by using z.infer<typeof GenerationRowSchema> (e.g., type GenerationRowRaw = z.infer<typeof GenerationRowSchema>) and then export/rename a cleaned type for callers (GenerationRow) that matches the rest of the code, and update the transformation that converts started_at to start_time to return the derived/renamed type so you no longer maintain two divergent definitions.
128-141: Timezone mismatch risk in sparkline bucket alignment.The bucket key generation uses
toISOString().slice(0, 13)which produces UTC timestamps, but ClickHouse'stoStartOfHour(start_time)may use the server's timezone setting. If they don't align, counts will be attributed to wrong buckets.Consider explicitly using UTC in the ClickHouse query:
💡 Suggested fix
query: `SELECT prompt_slug, - toStartOfHour(start_time) AS bucket, + formatDateTime(toStartOfHour(start_time), '%Y-%m-%d %H:00:00', 'UTC') AS bucket, count() AS cntAnd update the JavaScript to match:
bucketKeys.push( - h.toISOString().slice(0, 13).replace("T", " ") + ":00:00" + h.toISOString().slice(0, 13).replace("T", " ") + ":00:00" // Already UTC );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts` around lines 128 - 141, The bucket generation uses local-hour math then toISOString (UTC), which can misalign with ClickHouse; switch to pure-UTC calculations and make ClickHouse use UTC for hour bucketing. Concretely: in PromptPresenter.server.ts change the startHour computation and per-hour loop to compute times in milliseconds from Date.now() (or use getUTCHours/getUTC*), e.g. derive startUtcMs = Date.now() - 23*3600*1000 and build each bucket with new Date(startUtcMs + i*3600*1000) so toISOString() yields the intended UTC hour strings; and update the ClickHouse query to call toStartOfHour(start_time, 'UTC') (or the equivalent timezone param) so both JS bucketKeys and ClickHouse grouping use UTC consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a29af65c-2bdd-4a1f-b68d-9bc64a956682
⛔ Files ignored due to path filters (3)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlreferences/hello-world/package.jsonis excluded by!references/**references/hello-world/src/trigger/prompts.tsis excluded by!references/**
📒 Files selected for processing (74)
apps/webapp/app/components/BlankStatePanels.tsxapps/webapp/app/components/code/TSQLResultsTable.tsxapps/webapp/app/components/code/TextEditor.tsxapps/webapp/app/components/metrics/OperationsFilter.tsxapps/webapp/app/components/metrics/PromptsFilter.tsxapps/webapp/app/components/metrics/ProvidersFilter.tsxapps/webapp/app/components/metrics/QueryWidget.tsxapps/webapp/app/components/navigation/SideMenu.tsxapps/webapp/app/components/primitives/Resizable.tsxapps/webapp/app/components/runs/v3/PromptSpanDetails.tsxapps/webapp/app/components/runs/v3/SpanTitle.tsxapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsxapps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsxapps/webapp/app/components/runs/v3/ai/AIModelSummary.tsxapps/webapp/app/components/runs/v3/ai/AISpanDetails.tsxapps/webapp/app/components/runs/v3/ai/AIToolCallSpanDetails.tsxapps/webapp/app/components/runs/v3/ai/extractAISpanData.tsapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/components/runs/v3/ai/index.tsapps/webapp/app/components/runs/v3/ai/types.tsapps/webapp/app/presenters/v3/BuiltInDashboards.server.tsapps/webapp/app/presenters/v3/MetricDashboardPresenter.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.tsapps/webapp/app/presenters/v3/SpanPresenter.server.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/routes/api.v1.prompts.$slug.override.reactivate.tsapps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/routes/api.v1.prompts.$slug.promote.tsapps/webapp/app/routes/api.v1.prompts.$slug.tsapps/webapp/app/routes/api.v1.prompts.$slug.versions.tsapps/webapp/app/routes/api.v1.prompts._index.tsapps/webapp/app/routes/resources.metric.tsxapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug.generations.tsapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsxapps/webapp/app/services/authorization.server.tsapps/webapp/app/services/queryService.server.tsapps/webapp/app/utils/pathBuilder.tsapps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.tsapps/webapp/app/v3/eventRepository/eventRepository.types.tsapps/webapp/app/v3/querySchemas.tsapps/webapp/app/v3/services/createBackgroundWorker.server.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/v3/utils/enrichCreatableEvents.server.tsinternal-packages/clickhouse/schema/025_add_prompt_columns_to_llm_metrics_v1.sqlinternal-packages/clickhouse/src/llmMetrics.tsinternal-packages/database/prisma/migrations/20260317221618_add_prompt_management/migration.sqlinternal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sqlinternal-packages/database/prisma/schema.prismainternal-packages/llm-pricing/src/default-model-prices.jsoninternal-packages/llm-pricing/src/defaultPrices.tspackage.jsonpackages/cli-v3/src/dev/devSupervisor.tspackages/cli-v3/src/entryPoints/dev-index-worker.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/cli-v3/src/mcp/config.tspackages/cli-v3/src/mcp/tools.tspackages/cli-v3/src/mcp/tools/prompts.tspackages/core/src/v3/apiClient/index.tspackages/core/src/v3/resource-catalog/catalog.tspackages/core/src/v3/resource-catalog/index.tspackages/core/src/v3/resource-catalog/noopResourceCatalog.tspackages/core/src/v3/resource-catalog/standardResourceCatalog.tspackages/core/src/v3/schemas/api.tspackages/core/src/v3/schemas/build.tspackages/core/src/v3/schemas/resources.tspackages/core/src/v3/schemas/schemas.tspackages/core/src/v3/types/tasks.tspackages/trigger-sdk/src/v3/index.tspackages/trigger-sdk/src/v3/prompt.tspackages/trigger-sdk/src/v3/prompts.tspatches/@window-splitter__state@0.4.1.patch
| const copy = useCallback(() => { | ||
| if (view === undefined) return; | ||
| navigator.clipboard.writeText(view.state.doc.toString()); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }, [view]); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does navigator.clipboard.writeText() return a Promise that can reject, and in which cases (permissions/insecure context/user gesture)?
💡 Result:
Yes, navigator.clipboard.writeText returns a Promise that can reject with a NotAllowedError DOMException. It rejects in these cases: - Insecure context (non-HTTPS, non-localhost): navigator.clipboard is undefined, so calling writeText throws a TypeError (property undefined). - Secure context but permission denied: e.g., clipboard-write permission not granted (Chromium), or lacking transient user activation (user gesture like click; required by most browsers including Safari/Firefox strictly, Chromium with conditions). - Not in active tab or lacking document focus (browser-specific, e.g., Chromium requires active tab). - Permissions Policy disallows clipboard-write (e.g., in iframes without proper header). Always wrap in try-catch or .catch as shown in MDN examples.
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
- 2: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
- 3: https://web.dev/articles/async-clipboard
- 4: https://jsguides.dev/tutorials/browser-clipboard/
- 5: https://developer.mozilla.org/docs/Web/API/Clipboard_API
- 6: https://w3c.github.io/clipboard-apis/
- 7: https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined
- 8: Issue with "Clipboard.writeText()": doesn't work in HTTP protocol mdn/content#10741
- 9: https://bugzilla.mozilla.org/show_bug.cgi?id=1554855
- 10: User gesture requirement for Clipboard API access w3c/clipboard-apis#52
- 11: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard
- 12: Interoperability issue:
navigator.clipboard.write()andnavigator.clipboard.writeText()user gesture requirement w3c/clipboard-apis#182
🏁 Script executed:
# Check if the file exists and examine the exact code at lines 81-86
if [ -f "apps/webapp/app/components/code/TextEditor.tsx" ]; then
echo "File found. Showing lines 78-90 for context:"
sed -n '78,90p' "apps/webapp/app/components/code/TextEditor.tsx"
else
echo "File not found. Searching for TextEditor.tsx..."
find . -name "TextEditor.tsx" -type f 2>/dev/null | head -5
fiRepository: triggerdotdev/trigger.dev
Length of output: 434
Handle clipboard write failures before showing success
The navigator.clipboard.writeText() call on line 83 returns a Promise that can reject due to permission denial, insecure context, lacking user activation, or permissions policy violations. Without error handling, a rejection surfaces as an unhandled promise rejection and the "copied" UI state shows incorrectly.
Suggested change
- const copy = useCallback(() => {
+ const copy = useCallback(async () => {
if (view === undefined) return;
- navigator.clipboard.writeText(view.state.doc.toString());
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
+ try {
+ await navigator.clipboard.writeText(view.state.doc.toString());
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ } catch {
+ setCopied(false);
+ }
}, [view]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const copy = useCallback(() => { | |
| if (view === undefined) return; | |
| navigator.clipboard.writeText(view.state.doc.toString()); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| }, [view]); | |
| const copy = useCallback(async () => { | |
| if (view === undefined) return; | |
| try { | |
| await navigator.clipboard.writeText(view.state.doc.toString()); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| } catch { | |
| setCopied(false); | |
| } | |
| }, [view]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/code/TextEditor.tsx` around lines 81 - 86, The
copy function in TextEditor currently calls
navigator.clipboard.writeText(view.state.doc.toString()) without awaiting or
handling failures, causing unhandled promise rejections and incorrect setCopied
UI state; update the copy callback (the useCallback named copy) to await
navigator.clipboard.writeText(...) or chain .then/.catch, only call
setCopied(true) on success, call setCopied(false) or leave unchanged on failure,
and log or surface the error (e.g., via console.error or an existing logger) so
permission/context failures are handled gracefully.
| function OperationsDropdown({ | ||
| trigger, | ||
| clearSearchValue, | ||
| searchValue, | ||
| onClose, | ||
| possibleOperations, | ||
| }: { | ||
| trigger: ReactNode; | ||
| clearSearchValue: () => void; | ||
| searchValue: string; | ||
| onClose?: () => void; | ||
| possibleOperations: string[]; | ||
| }) { | ||
| const { values, replace } = useSearchParams(); | ||
|
|
||
| const handleChange = (values: string[]) => { | ||
| clearSearchValue(); | ||
| replace({ operations: values }); | ||
| }; | ||
|
|
||
| const filtered = useMemo(() => { | ||
| const q = searchValue.toLowerCase(); | ||
| return possibleOperations.filter( | ||
| (op) => op.toLowerCase().includes(q) || formatOperation(op).toLowerCase().includes(q) | ||
| ); | ||
| }, [searchValue, possibleOperations]); |
There was a problem hiding this comment.
Wire the combobox back to the search state.
filtered only reacts to searchValue, but OperationsDropdown only receives clearSearchValue and ComboBox only gets the current value. Typing here won't update the rendered list, so the operation search is effectively broken.
Also applies to: 113-125
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/metrics/OperationsFilter.tsx` around lines 85 -
110, The filtering is only tied to searchValue while the ComboBox input isn't
updating that state; update OperationsDropdown so the ComboBox input drives the
search used by filtered: either propagate ComboBox input changes into the search
param (useSearchParams values/replace) or maintain a local inputState in
OperationsDropdown and include it in the useMemo dependency list; ensure
ComboBox receives the current input value and an onInputChange that updates that
state (so filtered recalculates), and keep handleChange to update the selected
operations via replace({ operations: values }) and clearSearchValue as before;
adjust the logic around filtered, ComboBox props, and any references to
searchValue in OperationsDropdown/handleChange accordingly.
| function PromptsDropdown({ | ||
| trigger, | ||
| clearSearchValue, | ||
| searchValue, | ||
| onClose, | ||
| possiblePrompts, | ||
| }: { | ||
| trigger: ReactNode; | ||
| clearSearchValue: () => void; | ||
| searchValue: string; | ||
| onClose?: () => void; | ||
| possiblePrompts: string[]; | ||
| }) { | ||
| const { values, replace } = useSearchParams(); | ||
|
|
||
| const handleChange = (values: string[]) => { | ||
| clearSearchValue(); | ||
| replace({ prompts: values }); | ||
| }; | ||
|
|
||
| const filtered = useMemo(() => { | ||
| return possiblePrompts.filter((p) => { | ||
| return p.toLowerCase().includes(searchValue.toLowerCase()); | ||
| }); | ||
| }, [searchValue, possiblePrompts]); |
There was a problem hiding this comment.
Wire the combobox back to the search state.
filtered only reacts to searchValue, but PromptsDropdown only receives clearSearchValue and ComboBox only gets the current value. Typing here won't update the rendered list, so the prompt search is effectively broken.
Also applies to: 101-113
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/metrics/PromptsFilter.tsx` around lines 74 - 98,
PromptsDropdown's filtered list only depends on the local searchValue, but the
component reads/writes the persistent search state via useSearchParams (values,
replace) and the ComboBox is controlled by that state, so typing doesn't update
the rendered list; fix by reading the current prompts/search term from values
(values.prompts or a dedicated values.search key) and include that in the
filtered useMemo dependencies, ensure handleChange calls replace with the
updated prompts array/string, and make the ComboBox controlled by the same
search param (pass the parsed values.prompts as its value and wire its onChange
to handleChange/replace while still calling clearSearchValue where appropriate)
so filtered, ComboBox value, and URL/search params stay in sync (adjust parsing
if values.prompts is a string vs array).
| function ProvidersDropdown({ | ||
| trigger, | ||
| clearSearchValue, | ||
| searchValue, | ||
| onClose, | ||
| possibleProviders, | ||
| }: { | ||
| trigger: ReactNode; | ||
| clearSearchValue: () => void; | ||
| searchValue: string; | ||
| onClose?: () => void; | ||
| possibleProviders: string[]; | ||
| }) { | ||
| const { values, replace } = useSearchParams(); | ||
|
|
||
| const handleChange = (values: string[]) => { | ||
| clearSearchValue(); | ||
| replace({ providers: values }); | ||
| }; | ||
|
|
||
| const filtered = useMemo(() => { | ||
| return possibleProviders.filter((p) => p.toLowerCase().includes(searchValue.toLowerCase())); | ||
| }, [searchValue, possibleProviders]); |
There was a problem hiding this comment.
Wire the combobox back to the search state.
filtered only reacts to searchValue, but ProvidersDropdown only receives clearSearchValue and ComboBox only gets the current value. Typing here won't update the rendered list, so the provider search is effectively broken.
Also applies to: 99-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/metrics/ProvidersFilter.tsx` around lines 74 - 96,
ProvidersDropdown’s filtered list is only using the searchValue prop so typing
in the ComboBox doesn’t update the rendered list; change filtered to derive its
input from the search state returned by useSearchParams (the values object)
instead of — or in addition to — the searchValue prop, and wire the ComboBox
input handlers to update that search param via replace (and call
clearSearchValue where appropriate). Specifically, update the useMemo that
computes filtered to depend on values (or the exact search param key you use,
e.g. values.providerSearch or values.providers) and possibleProviders, and
ensure the ComboBox receives the controlled input from values and calls a
handler that calls replace(...) so typing updates values and recomputes
filtered; keep handleChange for committing provider selections.
| <SideMenuSection | ||
| title="AI" | ||
| isSideMenuCollapsed={isCollapsed} | ||
| itemSpacingClassName="space-y-0" | ||
| initialCollapsed={getSectionCollapsed( | ||
| user.dashboardPreferences.sideMenu, | ||
| "ai" | ||
| )} | ||
| onCollapseToggle={handleSectionToggle("ai")} | ||
| > | ||
| <SideMenuItem | ||
| name="Prompts" | ||
| icon={DocumentTextIcon} | ||
| activeIconColor="text-purple-500" | ||
| inactiveIconColor="text-purple-500" | ||
| to={v3PromptsPath(organization, project, environment)} | ||
| data-action="prompts" | ||
| isCollapsed={isCollapsed} | ||
| /> | ||
| <SideMenuItem | ||
| name="AI Metrics" | ||
| icon={SparklesIcon} | ||
| activeIconColor="text-purple-500" | ||
| inactiveIconColor="text-purple-500" | ||
| to={v3BuiltInDashboardPath(organization, project, environment, "llm")} | ||
| data-action="ai-metrics" | ||
| isCollapsed={isCollapsed} | ||
| /> | ||
| </SideMenuSection> | ||
|
|
There was a problem hiding this comment.
Gate “AI Metrics” behind the same query-access check used for dashboards.
Right now AI Metrics is always rendered, which can expose a nav path users may not be authorized to access.
Proposed fix
<SideMenuItem
name="Prompts"
icon={DocumentTextIcon}
activeIconColor="text-purple-500"
inactiveIconColor="text-purple-500"
to={v3PromptsPath(organization, project, environment)}
data-action="prompts"
isCollapsed={isCollapsed}
/>
- <SideMenuItem
- name="AI Metrics"
- icon={SparklesIcon}
- activeIconColor="text-purple-500"
- inactiveIconColor="text-purple-500"
- to={v3BuiltInDashboardPath(organization, project, environment, "llm")}
- data-action="ai-metrics"
- isCollapsed={isCollapsed}
- />
+ {(user.admin || user.isImpersonating || featureFlags.hasQueryAccess) && (
+ <SideMenuItem
+ name="AI Metrics"
+ icon={SparklesIcon}
+ activeIconColor="text-purple-500"
+ inactiveIconColor="text-purple-500"
+ to={v3BuiltInDashboardPath(organization, project, environment, "llm")}
+ data-action="ai-metrics"
+ isCollapsed={isCollapsed}
+ />
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/navigation/SideMenu.tsx` around lines 460 - 489,
The "AI Metrics" SideMenuItem is always rendered and should be conditionally
shown using the same query-access check used for dashboards; wrap the
SideMenuItem that links to v3BuiltInDashboardPath(...) in that existing
permission check (the same helper used when rendering the dashboards nav), so
only render the SideMenuItem when the check passes—leave the "Prompts" item and
the surrounding SideMenuSection, getSectionCollapsed and handleSectionToggle
logic unchanged.
| const url = new URL(request.url); | ||
| const versions = url.searchParams.getAll("versions").filter(Boolean).map(Number).filter((n) => !isNaN(n)); | ||
| const period = url.searchParams.get("period") ?? "7d"; | ||
| const fromTime = url.searchParams.get("from"); | ||
| const toTime = url.searchParams.get("to"); | ||
| const cursorParam = url.searchParams.get("cursor") ?? undefined; | ||
|
|
||
| const periodMs = parsePeriodToMs(period); | ||
| const startTime = fromTime ? new Date(fromTime) : new Date(Date.now() - periodMs); | ||
| const endTime = toTime ? new Date(toTime) : new Date(); | ||
|
|
||
| const models = url.searchParams.getAll("models").filter(Boolean); | ||
| const operations = url.searchParams.getAll("operations").filter(Boolean); | ||
| const providers = url.searchParams.getAll("providers").filter(Boolean); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the file exists and get its content
cat -n apps/webapp/app/routes/resources.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.prompts.\$promptSlug.generations.ts | head -100Repository: triggerdotdev/trigger.dev
Length of output: 3777
🏁 Script executed:
# Check how listGenerations uses these parameters
fd -t f "PromptPresenter" --exec grep -l "listGenerations" {} \;Repository: triggerdotdev/trigger.dev
Length of output: 126
🏁 Script executed:
# Check the PromptPresenter implementation
cat -n ./apps/webapp/app/presenters/v3/PromptPresenter.server.ts | head -150Repository: triggerdotdev/trigger.dev
Length of output: 5574
🏁 Script executed:
# Continue reading to find listGenerations method
cat -n ./apps/webapp/app/presenters/v3/PromptPresenter.server.ts | sed -n '150,350p'Repository: triggerdotdev/trigger.dev
Length of output: 7809
🏁 Script executed:
# Verify Invalid Date behavior by checking if toISOString is called
grep -A 5 "toISOString" ./apps/webapp/app/presenters/v3/PromptPresenter.server.ts | head -10Repository: triggerdotdev/trigger.dev
Length of output: 376
🏁 Script executed:
# Check if there are any zod schemas used in the loader for query validation
grep -B5 -A10 "loader = async" ./apps/webapp/app/routes/resources.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.prompts.\$promptSlug.generations.tsRepository: triggerdotdev/trigger.dev
Length of output: 715
🏁 Script executed:
# Verify what happens when Invalid Date is used - check if there are any safeguards
grep -rn "Invalid Date\|toISOString\|RangeError" ./apps/webapp/app/routes/resources.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.prompts.\$promptSlug.generations.tsRepository: triggerdotdev/trigger.dev
Length of output: 51
🏁 Script executed:
# Double-check the date parsing and usage one more time to be thorough
sed -n '52,80p' ./apps/webapp/app/routes/resources.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.prompts.\$promptSlug.generations.tsRepository: triggerdotdev/trigger.dev
Length of output: 1362
Use zod to validate query parameters before constructing the time window.
Query strings for from, to, versions, period, models, operations, and providers are parsed without validation. new Date(fromTime) and new Date(toTime) can produce Invalid Date objects, which will throw a RangeError when .toISOString() is called at line 300 in the presenter. Additionally, Number() accepts decimals and Infinity, not just valid prompt version integers. These invalid inputs should fail fast with a 400 response, not propagate into listGenerations(). Per coding guidelines, "Use zod for validation in packages/core and apps/webapp".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/webapp/app/routes/resources.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug.generations.ts
around lines 52 - 65, The query parameters (from/to, versions, period, models,
operations, providers) are parsed without validation which can yield Invalid
Date or bad numbers; add a zod schema at the top of the request handler in
generations.ts to validate and coerce these query params (validate period with
parsePeriodToMs-compatible values, parse versions as integers >0, ensure from/to
are valid ISO datetimes, and models/operations/providers are arrays of non-empty
strings), return a 400 response on schema failure, and only then construct
startTime/endTime (using new Date(...) on validated values) and call
listGenerations; reference parsePeriodToMs, the URL search params (versions,
period, from/to, models, operations, providers), and listGenerations when wiring
the validated values through.
| @@ -0,0 +1,5 @@ | |||
| -- AlterTable | |||
| ALTER TABLE "public"."prompts" ADD COLUMN "friendly_id" TEXT NOT NULL; | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the complete migration file
cat internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sqlRepository: triggerdotdev/trigger.dev
Length of output: 253
🏁 Script executed:
# Check if there are any related migrations that might create indexes on this column
fd "migration.sql" internal-packages/database/prisma/migrations | xargs grep -l "friendly_id" 2>/dev/nullRepository: triggerdotdev/trigger.dev
Length of output: 262
🏁 Script executed:
# Let's also check if there's any context about the prompts table structure
fd "migration.sql" internal-packages/database/prisma/migrations | xargs grep -l "CREATE TABLE.*prompts" 2>/dev/null | head -5Repository: triggerdotdev/trigger.dev
Length of output: 164
Separate this migration into two files: (1) add friendly_id column with staged rollout, (2) create unique index in separate migration with CONCURRENTLY.
Adding NOT NULL directly to an existing table without a staged migration (nullable column → backfill → add constraint) will fail on environments with existing data. Additionally, the unique index must be created in a separate migration file using CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS to avoid table locks in production.
Current migration structure (problematic)
ALTER TABLE "public"."prompts" ADD COLUMN "friendly_id" TEXT NOT NULL;
CREATE UNIQUE INDEX "prompts_friendly_id_key" ON "public"."prompts"("friendly_id");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sql`
at line 2, Split the single migration into two migrations: (1) change the ALTER
TABLE "prompts" ADD COLUMN "friendly_id" TEXT NOT NULL into a migration that
adds the column as nullable (ALTER TABLE ... ADD COLUMN "friendly_id" TEXT), run
a backfill step to populate friendly_id for existing rows, and then in a
follow-up migration add the NOT NULL constraint (ALTER TABLE ... ALTER COLUMN
"friendly_id" SET NOT NULL); (2) move the unique index creation into a separate
migration and create it using a non-blocking statement such as CREATE UNIQUE
INDEX CONCURRENTLY IF NOT EXISTS "prompts_friendly_id_key" ON
"public"."prompts"("friendly_id") so the index is built without locking the
table. Ensure the migrations reference the column name friendly_id, the table
prompts, and the index name prompts_friendly_id_key so they apply cleanly in
order.
| ALTER TABLE "public"."prompts" ADD COLUMN "friendly_id" TEXT NOT NULL; | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "prompts_friendly_id_key" ON "public"."prompts"("friendly_id"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify prompts-table migration history and current index statement usage
fd migration.sql internal-packages/database/prisma/migrations -x rg -n \
'CREATE TABLE "public"\."prompts"|ALTER TABLE "public"\."prompts"|prompts_friendly_id_key|CREATE UNIQUE INDEX'Repository: triggerdotdev/trigger.dev
Length of output: 23196
🏁 Script executed:
cat -n internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sqlRepository: triggerdotdev/trigger.dev
Length of output: 288
Split the index creation into a separate migration file using CONCURRENTLY.
Line 5 creates a unique index on an existing table in the same migration as the column addition. Per coding guidelines, this should be split into a separate migration file with CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "prompts_friendly_id_key" ON "public"."prompts"("friendly_id"); to avoid table locks in production.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sql`
at line 5, Move the unique index creation for "prompts_friendly_id_key" out of
this migration and into a new, separate migration that runs after the column
addition; in that new migration use CREATE UNIQUE INDEX CONCURRENTLY IF NOT
EXISTS "prompts_friendly_id_key" ON "public"."prompts"("friendly_id") so the
index is created without taking an exclusive lock on the existing "prompts"
table (reference the index name prompts_friendly_id_key, table "prompts" and
column "friendly_id" when creating the new migration).
| { | ||
| "id": "4bf01a9f-663f-4302-a05c-b2b42c5348e3", | ||
| "modelName": "gpt-5.4-mini", | ||
| "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini)$", | ||
| "createdAt": "2026-03-18T00:00:00.000Z", | ||
| "updatedAt": "2026-03-18T00:00:00.000Z", | ||
| "tokenizerConfig": { | ||
| "tokensPerName": 1, | ||
| "tokenizerModel": "gpt-4", | ||
| "tokensPerMessage": 3 | ||
| }, | ||
| "tokenizerId": "openai", | ||
| "pricingTiers": [ | ||
| { | ||
| "id": "4bf01a9f-663f-4302-a05c-b2b42c5348e3_tier_default", | ||
| "name": "Standard", | ||
| "isDefault": true, | ||
| "priority": 0, | ||
| "conditions": [], | ||
| "prices": { | ||
| "input": 0.75e-6, | ||
| "input_cached_tokens": 0.075e-6, | ||
| "input_cache_read": 0.075e-6, | ||
| "output": 4.5e-6 | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "3d8027df-7491-442e-9770-1363ab2452fc", | ||
| "modelName": "gpt-5.4-mini-2026-03-17", | ||
| "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", | ||
| "createdAt": "2026-03-18T00:00:00.000Z", | ||
| "updatedAt": "2026-03-18T00:00:00.000Z", | ||
| "tokenizerConfig": { | ||
| "tokensPerName": 1, | ||
| "tokenizerModel": "gpt-4", | ||
| "tokensPerMessage": 3 | ||
| }, | ||
| "tokenizerId": "openai", | ||
| "pricingTiers": [ | ||
| { | ||
| "id": "3d8027df-7491-442e-9770-1363ab2452fc_tier_default", | ||
| "name": "Standard", | ||
| "isDefault": true, | ||
| "priority": 0, | ||
| "conditions": [], | ||
| "prices": { | ||
| "input": 0.75e-6, | ||
| "input_cached_tokens": 0.075e-6, | ||
| "input_cache_read": 0.075e-6, | ||
| "output": 4.5e-6 | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "2d83d130-d0f1-4b5d-be1c-7caf70b8e444", | ||
| "modelName": "gpt-5.4-nano", | ||
| "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano)$", | ||
| "createdAt": "2026-03-18T00:00:00.000Z", | ||
| "updatedAt": "2026-03-18T00:00:00.000Z", | ||
| "tokenizerConfig": { | ||
| "tokensPerName": 1, | ||
| "tokenizerModel": "gpt-4", | ||
| "tokensPerMessage": 3 | ||
| }, | ||
| "tokenizerId": "openai", | ||
| "pricingTiers": [ | ||
| { | ||
| "id": "2d83d130-d0f1-4b5d-be1c-7caf70b8e444_tier_default", | ||
| "name": "Standard", | ||
| "isDefault": true, | ||
| "priority": 0, | ||
| "conditions": [], | ||
| "prices": { | ||
| "input": 0.2e-6, | ||
| "input_cached_tokens": 0.02e-6, | ||
| "input_cache_read": 0.02e-6, | ||
| "output": 1.25e-6 | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "33c7321b-c56e-45e9-9640-33ba3f5cb4fa", | ||
| "modelName": "gpt-5.4-nano-2026-03-17", | ||
| "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", | ||
| "createdAt": "2026-03-18T00:00:00.000Z", | ||
| "updatedAt": "2026-03-18T00:00:00.000Z", | ||
| "tokenizerConfig": { | ||
| "tokensPerName": 1, | ||
| "tokenizerModel": "gpt-4", | ||
| "tokensPerMessage": 3 | ||
| }, | ||
| "tokenizerId": "openai", | ||
| "pricingTiers": [ | ||
| { | ||
| "id": "33c7321b-c56e-45e9-9640-33ba3f5cb4fa_tier_default", | ||
| "name": "Standard", | ||
| "isDefault": true, | ||
| "priority": 0, | ||
| "conditions": [], | ||
| "prices": { | ||
| "input": 0.2e-6, | ||
| "input_cached_tokens": 0.02e-6, | ||
| "input_cache_read": 0.02e-6, | ||
| "output": 1.25e-6 | ||
| } | ||
| } | ||
| ] | ||
| }, |
There was a problem hiding this comment.
Add the missing reasoning price aliases for the new GPT-5.4 mini/nano entries.
Every other gpt-5* record in this table maps output_reasoning_tokens and output_reasoning to the same rate as output, but these four omit both keys. If usage for GPT-5.4 mini/nano is emitted under reasoning-specific details, those tokens will currently price at zero. Re-run pnpm run sync-prices after fixing the source data so the generated TypeScript stays aligned.
Representative fix — apply the same addition to all four new entries
"prices": {
"input": 0.75e-6,
"input_cached_tokens": 0.075e-6,
"input_cache_read": 0.075e-6,
- "output": 4.5e-6
+ "output": 4.5e-6,
+ "output_reasoning_tokens": 4.5e-6,
+ "output_reasoning": 4.5e-6
}Run the following to confirm which gpt-5* entries are missing the reasoning aliases:
#!/bin/bash
set -euo pipefail
python - <<'PY'
import json
from pathlib import Path
path = Path("internal-packages/llm-pricing/src/default-model-prices.json")
models = json.loads(path.read_text())
for model in models:
name = model["modelName"]
if not name.startswith("gpt-5"):
continue
prices = model["pricingTiers"][0]["prices"]
missing = [key for key in ("output_reasoning_tokens", "output_reasoning") if key not in prices]
if missing:
print(f"{name}: missing {', '.join(missing)}")
PY🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal-packages/llm-pricing/src/default-model-prices.json` around lines
3787 - 3898, The four new GPT-5.4 entries (modelName "gpt-5.4-mini",
"gpt-5.4-mini-2026-03-17", "gpt-5.4-nano", "gpt-5.4-nano-2026-03-17") are
missing the reasoning price aliases; update each pricingTiers[0].prices object
to add "output_reasoning_tokens" and "output_reasoning" with the same numeric
value as the existing "output" key (so reasoning tokens/pricing match output),
then re-run "pnpm run sync-prices" to regenerate TypeScript artifacts.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx (2)
294-305: Tab buttons work correctly but lack explicittype="button".While these buttons aren't inside a form (so they won't accidentally submit), adding explicit
type="button"is a defensive practice that prevents potential issues if the component is ever used inside a form context.♻️ Suggested change
{availableTabs.map((tab) => ( <button key={tab} + type="button" onClick={() => handleTabClick(tab)} className={`px-2.5 py-1 text-[11px] capitalize transition-colors ${🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx` around lines 294 - 305, The tab buttons in AIChatMessages.tsx (the button rendered inside the map that calls handleTabClick and uses activeTab) should explicitly include type="button" to prevent accidental form submission if this component is ever placed inside a form; update the JSX for the button elements that render the tabs to add the type="button" attribute.
103-104: Consider usinghrefinstead of deprecatedxlinkHref.The
xlink:hrefattribute is deprecated in SVG 2.0. Modern browsers support thehrefattribute directly. WhilexlinkHrefstill works for compatibility, you could simplify tohrefif legacy browser support isn't required.♻️ Suggested change
- <use xlinkHref={`${tablerSpritePath}#tabler-file-text-ai`} /> + <use href={`${tablerSpritePath}#tabler-file-text-ai`} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx` around lines 103 - 104, In AIChatMessages.tsx replace the deprecated SVG attribute usage on the <use> element by swapping xlinkHref for href (keep the same value `${tablerSpritePath}#tabler-file-text-ai`) so the <use> element uses href instead of xlinkHref; update any other occurrences of xlinkHref in this component to href to align with SVG2 and modern browsers while preserving the tablerSpritePath reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/webapp/app/routes/api.v1.prompts`.$slug.override.ts:
- Around line 27-32: The loader currently returns a plain 405 Response without
CORS headers; change it to return the 405 through the existing apiCors helper so
the response includes CORS headers (i.e., wrap the 405 Response or a json
payload with apiCors(request, ...)). Update the loader function (export async
function loader) to call apiCors(request, ...) for the non-OPTIONS branch so its
405 response matches the action's CORS-wrapped 405 behavior.
---
Nitpick comments:
In `@apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx`:
- Around line 294-305: The tab buttons in AIChatMessages.tsx (the button
rendered inside the map that calls handleTabClick and uses activeTab) should
explicitly include type="button" to prevent accidental form submission if this
component is ever placed inside a form; update the JSX for the button elements
that render the tabs to add the type="button" attribute.
- Around line 103-104: In AIChatMessages.tsx replace the deprecated SVG
attribute usage on the <use> element by swapping xlinkHref for href (keep the
same value `${tablerSpritePath}#tabler-file-text-ai`) so the <use> element uses
href instead of xlinkHref; update any other occurrences of xlinkHref in this
component to href to align with SVG2 and modern browsers while preserving the
tablerSpritePath reference.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b384998e-aeb3-458a-8cbf-f030090af235
📒 Files selected for processing (3)
apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsxapps/webapp/app/routes/api.v1.prompts.$slug.override.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
- GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
- GitHub Check: sdk-compat / Deno Runtime
- GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
- GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
- GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
- GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
- GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
- GitHub Check: typecheck / typecheck
- GitHub Check: sdk-compat / Bun Runtime
- GitHub Check: sdk-compat / Cloudflare Workers
- GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
- GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead
**/*.{ts,tsx}: Use task export syntax: export const myTask = task({ id: 'my-task', run: async (payload) => { ... } })
Use Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work - avoid DEPRECATED zodworker (Graphile-worker wrapper)
Prisma 6.14.0 client and schema use PostgreSQL in internal-packages/database - import only from Prisma client
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use zod for validation in packages/core and apps/webapp
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use function declarations instead of default exports
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
apps/webapp/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Access all environment variables through the
envexport ofenv.server.tsinstead of directly accessingprocess.envin the Trigger.dev webapp
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
apps/webapp/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
apps/webapp/**/*.{ts,tsx}: When importing from@trigger.dev/corein the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}
📄 CodeRabbit inference engine (AGENTS.md)
Format code using Prettier before committing
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
apps/webapp/app/routes/**/*.ts
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Use Remix flat-file route convention with dot-separated segments (e.g.,
api.v1.tasks.$taskId.trigger.tsfor/api/v1/tasks/:taskId/trigger)
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.ts
apps/{webapp,supervisor}/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
When modifying only server components (apps/webapp/, apps/supervisor/) with no package changes, add a .server-changes/ file instead of a changeset
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js}: Always import from@trigger.dev/sdkfor Trigger.dev tasks - never use@trigger.dev/sdk/v3or deprecated client.defineJob
Import subpaths only from@trigger.dev/core, never import from root
Add crumbs as you write code using //@crumbscomments or //#region@crumbsblocks for agentcrumbs debug tracing
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
apps/webapp/**/*.{ts,tsx,jsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server
Files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.tsapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
🧠 Learnings (5)
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/routes/**/*.ts : Use Remix flat-file route convention with dot-separated segments (e.g., `api.v1.tasks.$taskId.trigger.ts` for `/api/v1/tasks/:taskId/trigger`)
Applied to files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.ts
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to apps/webapp/**/*.{ts,tsx,jsx,js} : Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server
Applied to files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp
Applied to files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.ts
📚 Learning: 2026-03-13T13:45:39.411Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3213
File: apps/webapp/app/routes/admin.llm-models.missing.$model.tsx:19-21
Timestamp: 2026-03-13T13:45:39.411Z
Learning: In `apps/webapp/app/routes/admin.llm-models.missing.$model.tsx`, the `decodeURIComponent(params.model ?? "")` call is intentionally unguarded. Remix route params are decoded at the routing layer before reaching the loader, so malformed percent-encoding is rejected upstream. The page is also admin-only, so the risk is minimal and no try-catch is warranted.
Applied to files:
apps/webapp/app/routes/api.v1.prompts.$slug.override.ts
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.
Applied to files:
apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
🔇 Additional comments (8)
apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx (3)
27-31: LGTM!Clean type definition using
typeas per coding guidelines.
113-119: Accessibility attributes properly implemented.The expand/collapse button now includes
aria-labelandaria-expandedattributes, providing an accessible name for screen readers and communicating the current state.
223-230: Good fix for HTML nesting compliance.Changing from
<Paragraph>(which renders a<p>) to a<div>correctly addresses the HTML validation warning, sinceStreamdownRendererlikely produces block-level elements that cannot be nested inside<p>tags. The typography classes are preserved.apps/webapp/app/routes/api.v1.prompts.$slug.override.ts (5)
1-8: LGTM!Imports are correctly structured: Remix types from
@remix-run/server-runtime, Zod for validation, Prisma client, and service imports from appropriate local paths.
10-25: LGTM!Zod schemas properly define validation for route params and request bodies. Using
z.objectaligns with the coding guideline to prefer types over interfaces.
63-86: LGTM!The POST handler correctly wraps
request.json()in try-catch and validates withsafeParse. ThecreateOverridecall is intentionally not wrapped since it doesn't throwServiceValidationError— any database errors would correctly surface as 500s.
88-116: LGTM!The PUT/PATCH handler correctly catches
ServiceValidationErrorsinceupdateOverridethrows this when no active override exists. The error handling properly preserves the service's status code.
118-127: LGTM!The DELETE handler and method-not-allowed fallback are correctly implemented. The
Allowheader now includes all supported methods.
| export async function loader({ request }: LoaderFunctionArgs) { | ||
| if (request.method.toUpperCase() === "OPTIONS") { | ||
| return apiCors(request, json({})); | ||
| } | ||
| return new Response(null, { status: 405 }); | ||
| } |
There was a problem hiding this comment.
Loader's 405 response lacks CORS headers.
The loader returns a plain Response without CORS headers for non-OPTIONS methods (line 31), while the action wraps its 405 response with apiCors (line 123-126). This inconsistency means GET requests will receive a 405 without CORS headers, potentially causing CORS errors for browser clients.
🔧 Proposed fix to add CORS headers
export async function loader({ request }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
- return new Response(null, { status: 405 });
+ return apiCors(
+ request,
+ json({ error: "Method not allowed" }, { status: 405, headers: { Allow: "POST, PUT, PATCH, DELETE" } })
+ );
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/routes/api.v1.prompts`.$slug.override.ts around lines 27 -
32, The loader currently returns a plain 405 Response without CORS headers;
change it to return the 405 through the existing apiCors helper so the response
includes CORS headers (i.e., wrap the 405 Response or a json payload with
apiCors(request, ...)). Update the loader function (export async function
loader) to call apiCors(request, ...) for the non-OPTIONS branch so its 405
response matches the action's CORS-wrapped 405 behavior.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
apps/webapp/app/presenters/v3/PromptPresenter.server.ts (2)
322-327: Consider improving type safety and error logging context.The
as anycast bypasses TypeScript's type checking. Additionally, error logs would benefit from including the query context (environmentId, promptSlug) for easier debugging.♻️ Suggested improvement
- const [error, rows] = await queryFn(queryParams as any); + const [error, rows] = await queryFn( + queryParams as z.infer<typeof queryFn extends (...args: infer P) => unknown ? P[0] : never> + ); if (error) { - console.error("Prompt generations query failed:", error); + console.error("Prompt generations query failed:", { + error, + environmentId: options.environmentId, + promptSlug: options.promptSlug, + }); return { generations: [], pagination: {} }; }Alternatively, consider restructuring the params schema to avoid dynamic construction, or use a discriminated union approach.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts` around lines 322 - 327, The call to queryFn currently suppresses type checks by casting queryParams as any and logs errors without context; update the code to give queryParams a proper type matching queryFn’s parameter (or create a discriminated union/explicit interface used by queryFn) instead of using "as any", then call queryFn(queryParams) with the correctly typed object; also enrich the error log in the error branch to include context (e.g., environmentId and promptSlug) alongside the error so the console.error in this block clearly shows "Prompt generations query failed" plus environmentId and promptSlug and the error object, and ensure the return shape ({ generations, pagination }) stays the same.
362-365: Add error logging for consistency.Other methods (getUsageSparklines, listGenerations) log errors before returning fallback values. This method silently swallows errors, making debugging harder.
♻️ Suggested fix
const [error, rows] = await queryFn({ organizationId, projectId, environmentId }); if (error) { + console.error("getDistinctPromptSlugs query failed:", error); return []; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts` around lines 362 - 365, This block silently swallows errors from queryFn; before returning [], log the error like the other methods (getUsageSparklines, listGenerations) do: call the same logger used in this file (e.g., processLogger or the module logger) to log a descriptive message plus the error and relevant context (organizationId, projectId, environmentId) so you capture the failure details, then return the empty array as the fallback.apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts (1)
64-79: Parseai.promptonce and reuse it for bothitemsandmessageCount.
promptJsonis parsed twice through separate logic paths. Centralizing parse+derive avoids drift between displayeditemsandmessageCountbehavior.Also applies to: 144-193
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts` around lines 64 - 79, Parse ai.prompt once into a single variable (e.g., parsedPrompt or parsedPromptJson) and reuse it for both items and messageCount to avoid double-parsing; replace the current separate JSON.parse usage with a single parse step, pass that parsed object into parsePromptToDisplayItems (instead of re-stringifying) and compute messageCount from the same parsed object (checking parsed.messages array or falling back to system/prompt fields), and apply the same change for the similar logic around the block at lines 144-193 so both display items and message count are derived from the same parsedPrompt.apps/webapp/app/v3/services/promptService.server.ts (1)
1-5: Move this service to the folderized v3 service layout.
apps/webapp/app/v3/services/promptService.server.tsdoes not follow theapp/v3/services/*/*.server.tsconvention used for service organization.As per coding guidelines
apps/webapp/app/v3/services/**/*.server.{ts,tsx}: Organize services in the webapp following the pattern app/v3/services/*/*.server.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/webapp/app/v3/services/promptService.server.ts` around lines 1 - 5, The PromptService class currently lives flat and must be moved into the v3 services folderized layout; create a new subfolder for this domain (e.g., a prompt subfolder) and move the PromptService (class PromptService) file there, keeping its exports and preserving imports of createHash, prisma, BaseService and ServiceValidationError; update all import sites that reference the old module path to the new subfolder path and adjust any relative import paths inside the moved file so imports (createHash, prisma, BaseService, ServiceValidationError) continue to resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts`:
- Around line 15-34: The current guard only checks truthiness of ai.operationId
but can allow non-string values and later produce an empty operationName; update
the early check to explicitly verify ai.operationId is a non-empty string (e.g.,
typeof ai.operationId === "string" && ai.operationId.trim() !== "") and return
undefined if it fails, and then use that validated value for operationName (via
str(ai.operationId)) so AISpanData always receives a valid non-empty
operationName; modify the checks around ai.operationId and the assignment to
operationName in extractAISummarySpanData to implement this.
- Around line 225-242: resolvedProvider is declared but never assigned; populate
it from the parsed metadata before returning. In extractAISummarySpanData
(variable resolvedProvider), set resolvedProvider = parsed.provider ||
parsed.openai?.provider || parsed.openai?.resolvedProvider ||
parsed.anthropic?.provider || parsed.anthropic?.resolvedProvider (or other
provider-specific field names your metadata uses) so the return expression
(serviceTier || resolvedProvider || responseId) can actually surface a provider
value; ensure you check each possible location on the parsed object (parsed,
parsed.openai, parsed.anthropic) and only assign when a string exists.
- Around line 149-150: Replace all manual JSON.parse + typeof checks in
extractAISummarySpanData with zod schemas: define Zod schemas that match the
expected shapes for promptJson (used where const parsed = JSON.parse(promptJson)
as Record<string, unknown>), the data parsed around lines 221–223, and the
structures parsed at 252–266; use schema.safeParse(...) instead of JSON.parse
and guard on result.success, returning undefined or typed value on failure.
Update references to the parsed variables (e.g., parsed) to use the typed .data
from safeParse so callers of extractAISummarySpanData (and local helpers) get
proper typed objects.
In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts`:
- Around line 100-141: The ClickHouse bucket timestamps are produced in the
server timezone while bucketKeys are generated in UTC (toISOString), causing
mismatches; update the query to force UTC on the aggregation (e.g., change
toStartOfHour(start_time) to toStartOfHour(toTimeZone(start_time, 'UTC')) or
toStartOfHour(start_time, 'UTC')) so the returned bucket field uses UTC, leaving
the bucketKeys generation (h.toISOString().slice(0,13).replace("T"," ") +
":00:00") as-is; modify the query SQL in the query object where toStartOfHour is
used so prompt usage sparkline rows align with the UTC bucketKeys.
In `@apps/webapp/app/v3/services/promptService.server.ts`:
- Around line 6-24: promoteVersion currently loads the version by versionId but
then mutates labels by promptId, which can relabel the wrong prompt and leave
partial state; modify promoteVersion to first load the PromptVersion via
this._prisma.promptVersion.findUnique (or include promptId) and verify
target.promptId === promptId (throw ServiceValidationError if not), then perform
the relabeling inside a single database transaction (use
this._prisma.$transaction) so `#removeLabel` and `#addLabel` are executed atomically
(or replace with a single transactional update that clears "current" on the old
prompt and sets "current" on the target version) to avoid partial state.
---
Nitpick comments:
In `@apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts`:
- Around line 64-79: Parse ai.prompt once into a single variable (e.g.,
parsedPrompt or parsedPromptJson) and reuse it for both items and messageCount
to avoid double-parsing; replace the current separate JSON.parse usage with a
single parse step, pass that parsed object into parsePromptToDisplayItems
(instead of re-stringifying) and compute messageCount from the same parsed
object (checking parsed.messages array or falling back to system/prompt fields),
and apply the same change for the similar logic around the block at lines
144-193 so both display items and message count are derived from the same
parsedPrompt.
In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts`:
- Around line 322-327: The call to queryFn currently suppresses type checks by
casting queryParams as any and logs errors without context; update the code to
give queryParams a proper type matching queryFn’s parameter (or create a
discriminated union/explicit interface used by queryFn) instead of using "as
any", then call queryFn(queryParams) with the correctly typed object; also
enrich the error log in the error branch to include context (e.g., environmentId
and promptSlug) alongside the error so the console.error in this block clearly
shows "Prompt generations query failed" plus environmentId and promptSlug and
the error object, and ensure the return shape ({ generations, pagination })
stays the same.
- Around line 362-365: This block silently swallows errors from queryFn; before
returning [], log the error like the other methods (getUsageSparklines,
listGenerations) do: call the same logger used in this file (e.g., processLogger
or the module logger) to log a descriptive message plus the error and relevant
context (organizationId, projectId, environmentId) so you capture the failure
details, then return the empty array as the fallback.
In `@apps/webapp/app/v3/services/promptService.server.ts`:
- Around line 1-5: The PromptService class currently lives flat and must be
moved into the v3 services folderized layout; create a new subfolder for this
domain (e.g., a prompt subfolder) and move the PromptService (class
PromptService) file there, keeping its exports and preserving imports of
createHash, prisma, BaseService and ServiceValidationError; update all import
sites that reference the old module path to the new subfolder path and adjust
any relative import paths inside the moved file so imports (createHash, prisma,
BaseService, ServiceValidationError) continue to resolve.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 7e552d78-3916-4562-b6c5-46a43cf3f9e5
📒 Files selected for processing (6)
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/presenters/v3/PromptPresenter.server.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/v3/utils/enrichCreatableEvents.server.ts
✅ Files skipped from review due to trivial changes (1)
- apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
- GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
- GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
- GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
- GitHub Check: sdk-compat / Bun Runtime
- GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
- GitHub Check: sdk-compat / Cloudflare Workers
- GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
- GitHub Check: sdk-compat / Deno Runtime
- GitHub Check: typecheck / typecheck
- GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
- GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
- GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (14)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead
**/*.{ts,tsx}: Use task export syntax: export const myTask = task({ id: 'my-task', run: async (payload) => { ... } })
Use Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work - avoid DEPRECATED zodworker (Graphile-worker wrapper)
Prisma 6.14.0 client and schema use PostgreSQL in internal-packages/database - import only from Prisma client
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use zod for validation in packages/core and apps/webapp
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use function declarations instead of default exports
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
apps/webapp/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Access all environment variables through the
envexport ofenv.server.tsinstead of directly accessingprocess.envin the Trigger.dev webapp
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
apps/webapp/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
apps/webapp/**/*.{ts,tsx}: When importing from@trigger.dev/corein the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}
📄 CodeRabbit inference engine (AGENTS.md)
Format code using Prettier before committing
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
apps/{webapp,supervisor}/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
When modifying only server components (apps/webapp/, apps/supervisor/) with no package changes, add a .server-changes/ file instead of a changeset
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js}: Always import from@trigger.dev/sdkfor Trigger.dev tasks - never use@trigger.dev/sdk/v3or deprecated client.defineJob
Import subpaths only from@trigger.dev/core, never import from root
Add crumbs as you write code using //@crumbscomments or //#region@crumbsblocks for agentcrumbs debug tracing
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
apps/webapp/**/*.{ts,tsx,jsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server
Files:
apps/webapp/app/components/navigation/sideMenuTypes.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsapps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
apps/webapp/app/v3/services/**/*.server.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Organize services in the webapp following the pattern
app/v3/services/*/*.server.ts
Files:
apps/webapp/app/v3/services/promptService.server.ts
apps/webapp/**/*.server.ts
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Access environment variables via the
envexport fromapp/env.server.ts, never useprocess.envdirectly
Files:
apps/webapp/app/v3/services/promptService.server.tsapps/webapp/app/presenters/v3/PromptPresenter.server.ts
apps/webapp/app/v3/services/**/*.server.ts
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
When editing services that branch on
RunEngineVersionto support both V1 and V2 (e.g.,cancelTaskRun.server.ts,batchTriggerV3.server.ts), only modify V2 code paths
Files:
apps/webapp/app/v3/services/promptService.server.ts
apps/webapp/app/v3/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
When modifying V3 code paths in apps/webapp/app/v3/, only modify V2 code - consult apps/webapp/CLAUDE.md for V1-only legacy code to avoid
Files:
apps/webapp/app/v3/services/promptService.server.ts
🧠 Learnings (18)
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to {packages/core,apps/webapp}/**/*.{ts,tsx} : Use zod for validation in packages/core and apps/webapp
Applied to files:
apps/webapp/app/components/navigation/sideMenuTypes.ts
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-11T16:50:14.167Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx:126-131
Timestamp: 2026-02-11T16:50:14.167Z
Learning: In apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx, MetricsDashboard entities are intentionally scoped to the organization level, not the project level. The dashboard lookup should filter by organizationId only (not projectId), allowing dashboards to be accessed across projects within the same organization. The optional projectId field on MetricsDashboard serves other purposes and should not be used as an authorization constraint.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to **/*.{ts,tsx} : Use Run Engine 2.0 (internal/run-engine) and redis-worker for all new work - avoid DEPRECATED zodworker (Graphile-worker wrapper)
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-10T16:18:48.654Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 2980
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx:512-515
Timestamp: 2026-02-10T16:18:48.654Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx, environment.queueSizeLimit is a per-queue maximum that is configured at the environment level, not a shared limit across all queues. Each queue can have up to environment.queueSizeLimit items queued independently.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/routes/**/*.ts : Use Remix flat-file route convention with dot-separated segments (e.g., `api.v1.tasks.$taskId.trigger.ts` for `/api/v1/tasks/:taskId/trigger`)
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `metadata.parent` and `metadata.root` to update parent and root task metadata from child tasks
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-03-13T13:43:06.471Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3213
File: apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts:52-52
Timestamp: 2026-03-13T13:43:06.471Z
Learning: In the trigger.dev codebase (PR `#3213`), `extractAISpanData.ts` (`apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts`) is a read-side UI helper that reads already-enriched `trigger.llm.*` span attributes for display. The actual LLM cost computation and gateway/OpenRouter cost fallback logic lives in `enrichCreatableEvents.server.ts` (`apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts`) via `extractProviderCost()`. The `gatewayCost` parsed in `extractAISpanData` is for UI display purposes only, not for cost calculation.
Applied to files:
apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/v3/services/**/*.server.ts : When editing services that branch on `RunEngineVersion` to support both V1 and V2 (e.g., `cancelTaskRun.server.ts`, `batchTriggerV3.server.ts`), only modify V2 code paths
Applied to files:
apps/webapp/app/v3/services/promptService.server.ts
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Applies to packages/cli-v3/src/commands/promote.ts : Implement `promote.ts` command in `src/commands/` for deployment promotion
Applied to files:
apps/webapp/app/v3/services/promptService.server.ts
📚 Learning: 2026-03-10T17:56:20.938Z
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 3201
File: apps/webapp/app/v3/services/setSeatsAddOn.server.ts:25-29
Timestamp: 2026-03-10T17:56:20.938Z
Learning: Do not implement local userId-to-organizationId authorization checks inside org-scoped service classes (e.g., SetSeatsAddOnService, SetBranchesAddOnService) in the web app. Rely on route-layer authentication (requireUserId(request)) and org membership enforcement via the _app.orgs.$organizationSlug layout route. Any userId/organizationId that reaches these services from org-scoped routes has already been validated. Apply this pattern across all org-scoped services to avoid redundant auth checks and maintain consistency.
Applied to files:
apps/webapp/app/v3/services/promptService.server.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/v3/presenters/**/*.server.{ts,tsx} : Organize presenters in the webapp following the pattern `app/v3/presenters/*/*.server.ts` to move complex loader code into classes
Applied to files:
apps/webapp/app/presenters/v3/PromptPresenter.server.ts
📚 Learning: 2025-07-12T18:06:04.133Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2264
File: apps/webapp/app/services/runsRepository.server.ts:172-174
Timestamp: 2025-07-12T18:06:04.133Z
Learning: In apps/webapp/app/services/runsRepository.server.ts, the in-memory status filtering after fetching runs from Prisma is intentionally used as a workaround for ClickHouse data delays. This approach is acceptable because the result set is limited to a maximum of 100 runs due to pagination, making the performance impact negligible.
Applied to files:
apps/webapp/app/presenters/v3/PromptPresenter.server.ts
📚 Learning: 2026-02-06T19:53:38.843Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts:233-237
Timestamp: 2026-02-06T19:53:38.843Z
Learning: When constructing Vercel dashboard URLs from deployment IDs, always strip the dpl_ prefix from the ID. Implement this by transforming the ID with .replace(/^dpl_/, "") before concatenating into the URL: https://vercel.com/${teamSlug}/${projectName}/${cleanedDeploymentId}. Consider centralizing this logic in a small helper (e.g., getVercelDeploymentId(id) or a URL builder) and add tests to verify both prefixed and non-prefixed inputs.
Applied to files:
apps/webapp/app/presenters/v3/PromptPresenter.server.ts
🔇 Additional comments (14)
apps/webapp/app/components/navigation/sideMenuTypes.ts (1)
4-4: Looks good — schema and inferred type stay in sync with the new AI section.Adding
"ai"toSideMenuSectionIdSchemacleanly updates both validation andSideMenuSectionIdwithout extra type maintenance.apps/webapp/app/presenters/v3/PromptPresenter.server.ts (7)
1-36: LGTM!The imports and schema definitions are well-structured. The zod schema properly uses
z.coerce.number()for numeric fields that may come as strings from ClickHouse. The field name difference betweenstarted_at(schema) andstart_time(exported type) is handled correctly via mapping at lines 332-335.
38-44: LGTM!Constructor properly initializes the ClickHouse client and passes the replica to the base presenter.
46-90: LGTM!Clean implementation with proper model resolution priority (override → current → default). The Prisma query efficiently fetches only relevant versions using the
hasSomefilter.
157-193: LGTM!Version resolution logic correctly prioritizes: explicit version → override label → specified/default label. The ordering by version descending ensures the latest matching version is returned.
195-212: LGTM!Clean implementation with appropriate field selection and sensible default limit.
229-233: LGTM!The keyset pagination is correctly implemented using a composite cursor (start_time, span_id) that handles timestamp ties properly.
Also applies to: 275-276, 338-341
370-384: LGTM!Cursor encoding/decoding is well-implemented with proper error handling for malformed input and compact property names to minimize cursor size.
apps/webapp/app/v3/services/promptService.server.ts (2)
120-134: Label helpers are idempotent and safely guarded.
array_remove/array_appendwithANY("labels")checks keeps label updates safe from duplicate append/remove behavior.
37-40: [Rewritten comment]
[Exactly ONE classification tag]apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (4)
54-54: LGTM!Clean import of the
ResizableSnapshottype and consistent destructuring pattern for the newresizableloader data.Also applies to: 307-308
512-512: LGTM – type assertions for serialized loader data.The
as ResizableSnapshotassertions are appropriate here sinceSerializeFrom<typeof loader>alters the types during serialization. This is a standard Remix pattern for cookie/storage-persisted data.Also applies to: 542-542
568-568: LGTM!Consistent implementation with
TraceView– the resizable snapshot persistence is correctly wired for the no-logs fallback view.Also applies to: 588-588
679-680: LGTM!The optional
treeSnapshotprop is correctly typed and properly passed to the innerResizablePanelGroup. This enables persisting the tree/timeline split sizes independently from the parent panel.Also applies to: 780-780
| if (!ai.operationId) return undefined; | ||
|
|
||
| // Skip child spans that have gen_ai.* (those use extractAISpanData) | ||
| if (properties.gen_ai && typeof properties.gen_ai === "object") return undefined; | ||
|
|
||
| const aiModel = rec(ai.model); | ||
| const aiResponse = rec(ai.response); | ||
| const aiUsage = rec(ai.usage); | ||
| const aiSettings = rec(ai.settings); | ||
| const aiRequest = rec(ai.request); | ||
| const aiTelemetry = rec(ai.telemetry); | ||
| const trigger = rec(properties.trigger); | ||
| const triggerLlm = rec(trigger.llm); | ||
|
|
||
| const model = str(aiModel.id); | ||
| if (!model) return undefined; | ||
|
|
||
| const provider = str(aiModel.provider) ?? "unknown"; | ||
| const operationName = str(ai.operationId) ?? ""; | ||
|
|
There was a problem hiding this comment.
Tighten ai.operationId validation to avoid empty operationName.
Line 15 checks truthiness only; a non-string truthy value passes, and Line 33 then falls back to "". Since operationName is required on AISpanData, guard for string upfront and return undefined otherwise.
Proposed fix
const ai = rec(properties.ai);
- if (!ai.operationId) return undefined;
+ const operationName = str(ai.operationId);
+ if (!operationName) return undefined;
@@
- const operationName = str(ai.operationId) ?? "";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts` around
lines 15 - 34, The current guard only checks truthiness of ai.operationId but
can allow non-string values and later produce an empty operationName; update the
early check to explicitly verify ai.operationId is a non-empty string (e.g.,
typeof ai.operationId === "string" && ai.operationId.trim() !== "") and return
undefined if it fails, and then use that validated value for operationName (via
str(ai.operationId)) so AISpanData always receives a valid non-empty
operationName; modify the checks around ai.operationId and the assignment to
operationName in extractAISummarySpanData to implement this.
| const parsed = JSON.parse(promptJson) as Record<string, unknown>; | ||
| if (!parsed || typeof parsed !== "object") return undefined; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify current manual JSON parsing and whether zod is used in this area.
rg -n --type=ts 'JSON\.parse\(|as Record<string, unknown>|typeof .*=== "object"' apps/webapp/app/components/runs/v3/ai
rg -n --type=ts 'from "zod"|from '\''zod'\''' apps/webapp/app/components/runs/v3/aiRepository: triggerdotdev/trigger.dev
Length of output: 4179
🏁 Script executed:
# Read the specific file to see exact code at flagged lines
sed -n '145,155p' apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts
sed -n '218,225p' apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts
sed -n '249,270p' apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.tsRepository: triggerdotdev/trigger.dev
Length of output: 1329
🏁 Script executed:
# Check if similar validation patterns use zod elsewhere in webapp
rg -n 'import.*from.*zod' apps/webapp/app/components/runs/ -l | head -5
rg -n 'z\.parse\(|safeParse\(' apps/webapp/app/components/runs/v3/ -l | head -5Repository: triggerdotdev/trigger.dev
Length of output: 391
🏁 Script executed:
# Check if extractAISpanData.ts (similar file) uses zod
head -30 apps/webapp/app/components/runs/v3/ai/extractAISpanData.tsRepository: triggerdotdev/trigger.dev
Length of output: 1190
Replace manual JSON validation with zod schemas throughout this file.
This file uses repeated patterns of JSON.parse() with type assertions and manual typeof guards for validation. Per coding guidelines, apps/webapp must use zod for validation. Apply this to all parsing instances including lines 149–150, 221–223, and 252–266.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts` around
lines 149 - 150, Replace all manual JSON.parse + typeof checks in
extractAISummarySpanData with zod schemas: define Zod schemas that match the
expected shapes for promptJson (used where const parsed = JSON.parse(promptJson)
as Record<string, unknown>), the data parsed around lines 221–223, and the
structures parsed at 252–266; use schema.safeParse(...) instead of JSON.parse
and guard on result.success, returning undefined or typed value on failure.
Update references to the parsed variables (e.g., parsed) to use the typed .data
from safeParse so callers of extractAISummarySpanData (and local helpers) get
proper typed objects.
| let serviceTier: string | undefined; | ||
| let resolvedProvider: string | undefined; | ||
| let responseId: string | undefined; | ||
|
|
||
| // OpenAI | ||
| if (parsed.openai) { | ||
| serviceTier = parsed.openai.serviceTier; | ||
| responseId = parsed.openai.responseId; | ||
| } | ||
|
|
||
| // Anthropic | ||
| if (parsed.anthropic?.usage?.service_tier) { | ||
| serviceTier = parsed.anthropic.usage.service_tier; | ||
| } | ||
|
|
||
| return serviceTier || resolvedProvider || responseId | ||
| ? { serviceTier, resolvedProvider, responseId } | ||
| : undefined; |
There was a problem hiding this comment.
resolvedProvider is never populated in provider metadata parsing.
resolvedProvider is declared (Line 226) and returned (Line 241) but never assigned, so this field is always undefined even when metadata contains it.
Proposed fix
function parseProviderMetadata(
raw: unknown
): { serviceTier?: string; resolvedProvider?: string; responseId?: string } | undefined {
const jsonStr = str(raw);
if (!jsonStr) return undefined;
try {
- const parsed = JSON.parse(jsonStr) as Record<string, any>;
+ const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
if (!parsed || typeof parsed !== "object") return undefined;
let serviceTier: string | undefined;
let resolvedProvider: string | undefined;
let responseId: string | undefined;
@@
if (parsed.anthropic?.usage?.service_tier) {
serviceTier = parsed.anthropic.usage.service_tier;
}
+ // Gateway/OpenRouter-style resolved provider hints
+ const openrouter = rec((parsed as Record<string, unknown>).openrouter);
+ const gateway = rec((parsed as Record<string, unknown>).gateway);
+ resolvedProvider =
+ str((parsed as Record<string, unknown>).resolvedProvider) ??
+ str(openrouter.provider) ??
+ str(gateway.provider);
+
return serviceTier || resolvedProvider || responseId
? { serviceTier, resolvedProvider, responseId }
: undefined;
} catch {
return undefined;
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts` around
lines 225 - 242, resolvedProvider is declared but never assigned; populate it
from the parsed metadata before returning. In extractAISummarySpanData (variable
resolvedProvider), set resolvedProvider = parsed.provider ||
parsed.openai?.provider || parsed.openai?.resolvedProvider ||
parsed.anthropic?.provider || parsed.anthropic?.resolvedProvider (or other
provider-specific field names your metadata uses) so the return expression
(serviceTier || resolvedProvider || responseId) can actually surface a provider
value; ensure you check each possible location on the parsed object (parsed,
parsed.openai, parsed.anthropic) and only assign when a string exists.
| query: `SELECT | ||
| prompt_slug, | ||
| toStartOfHour(start_time) AS bucket, | ||
| count() AS cnt | ||
| FROM trigger_dev.llm_metrics_v1 | ||
| WHERE environment_id = {environmentId: String} | ||
| AND prompt_slug IN {promptSlugs: Array(String)} | ||
| AND start_time >= now() - INTERVAL 24 HOUR | ||
| GROUP BY prompt_slug, bucket | ||
| ORDER BY prompt_slug, bucket`, | ||
| params: z.object({ | ||
| environmentId: z.string(), | ||
| promptSlugs: z.array(z.string()), | ||
| }), | ||
| schema: z.object({ | ||
| prompt_slug: z.string(), | ||
| bucket: z.string(), | ||
| cnt: z.coerce.number(), | ||
| }), | ||
| }); | ||
|
|
||
| const [error, rows] = await queryFn({ environmentId, promptSlugs }); | ||
| if (error) { | ||
| console.error("Prompt usage sparkline query failed:", error); | ||
| return {}; | ||
| } | ||
|
|
||
| // Build a map of slug -> 24 hourly buckets | ||
| const now = new Date(); | ||
| const startHour = new Date(now); | ||
| startHour.setMinutes(0, 0, 0); | ||
| startHour.setHours(startHour.getHours() - 23); | ||
|
|
||
| const bucketKeys: string[] = []; | ||
| for (let i = 0; i < 24; i++) { | ||
| const h = new Date(startHour); | ||
| h.setHours(h.getHours() + i); | ||
| // Format to match ClickHouse's toStartOfHour output: "YYYY-MM-DD HH:MM:SS" | ||
| bucketKeys.push( | ||
| h.toISOString().slice(0, 13).replace("T", " ") + ":00:00" | ||
| ); | ||
| } |
There was a problem hiding this comment.
Timezone mismatch between ClickHouse and JavaScript bucket keys.
The bucket keys are generated using toISOString() (UTC), but ClickHouse's toStartOfHour(start_time) returns values in the server's configured timezone. If the ClickHouse server isn't set to UTC, bucket keys won't match and sparkline data will misalign or appear as zeros.
🔧 Proposed fix to enforce UTC in ClickHouse query
query: `SELECT
prompt_slug,
- toStartOfHour(start_time) AS bucket,
+ toStartOfHour(start_time, 'UTC') AS bucket,
count() AS cnt
FROM trigger_dev.llm_metrics_v1🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/presenters/v3/PromptPresenter.server.ts` around lines 100 -
141, The ClickHouse bucket timestamps are produced in the server timezone while
bucketKeys are generated in UTC (toISOString), causing mismatches; update the
query to force UTC on the aggregation (e.g., change toStartOfHour(start_time) to
toStartOfHour(toTimeZone(start_time, 'UTC')) or toStartOfHour(start_time,
'UTC')) so the returned bucket field uses UTC, leaving the bucketKeys generation
(h.toISOString().slice(0,13).replace("T"," ") + ":00:00") as-is; modify the
query SQL in the query object where toStartOfHour is used so prompt usage
sparkline rows align with the UTC bucketKeys.
| async promoteVersion(promptId: string, versionId: string, options?: { sourceGuard?: boolean }) { | ||
| if (options?.sourceGuard) { | ||
| const target = await this._prisma.promptVersion.findUnique({ | ||
| where: { id: versionId }, | ||
| }); | ||
| if (!target) { | ||
| throw new ServiceValidationError("Version not found", 404); | ||
| } | ||
| if (target.source !== "code") { | ||
| throw new ServiceValidationError( | ||
| "Only code-sourced versions can be promoted. Use the override API instead.", | ||
| 400 | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| await this.#removeLabel(promptId, "current"); | ||
| await this.#addLabel(versionId, "current"); | ||
| } |
There was a problem hiding this comment.
Validate prompt ownership and make relabel operations atomic.
Line 8 / Line 101 load by versionId only, while Line 22-23 / Line 116-117 mutate labels using promptId from input. A mismatched versionId can remove labels from one prompt and apply to another. Also, remove/add should be a single transaction to avoid partial state.
🔧 Proposed fix
async promoteVersion(promptId: string, versionId: string, options?: { sourceGuard?: boolean }) {
- if (options?.sourceGuard) {
- const target = await this._prisma.promptVersion.findUnique({
- where: { id: versionId },
- });
- if (!target) {
- throw new ServiceValidationError("Version not found", 404);
- }
- if (target.source !== "code") {
- throw new ServiceValidationError(
- "Only code-sourced versions can be promoted. Use the override API instead.",
- 400
- );
- }
- }
-
- await this.#removeLabel(promptId, "current");
- await this.#addLabel(versionId, "current");
+ await this._prisma.$transaction(async (tx) => {
+ const target = await tx.promptVersion.findUnique({ where: { id: versionId } });
+ if (!target) throw new ServiceValidationError("Version not found", 404);
+ if (target.promptId !== promptId) {
+ throw new ServiceValidationError("Version does not belong to prompt", 400);
+ }
+ if (options?.sourceGuard && target.source !== "code") {
+ throw new ServiceValidationError(
+ "Only code-sourced versions can be promoted. Use the override API instead.",
+ 400
+ );
+ }
+
+ await tx.$executeRaw`
+ UPDATE "prompt_versions"
+ SET "labels" = array_remove("labels", 'current')
+ WHERE "promptId" = ${promptId} AND 'current' = ANY("labels")
+ `;
+ await tx.$executeRaw`
+ UPDATE "prompt_versions"
+ SET "labels" = array_append("labels", 'current')
+ WHERE "id" = ${versionId} AND NOT ('current' = ANY("labels"))
+ `;
+ });
}Also applies to: 100-118
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/webapp/app/v3/services/promptService.server.ts` around lines 6 - 24,
promoteVersion currently loads the version by versionId but then mutates labels
by promptId, which can relabel the wrong prompt and leave partial state; modify
promoteVersion to first load the PromptVersion via
this._prisma.promptVersion.findUnique (or include promptId) and verify
target.promptId === promptId (throw ServiceValidationError if not), then perform
the relabeling inside a single database transaction (use
this._prisma.$transaction) so `#removeLabel` and `#addLabel` are executed atomically
(or replace with a single transactional update that clears "current" on the old
prompt and sets "current" on the target version) to avoid partial state.
prompts.define()Prompt management
Define prompts in your code with
prompts.define(), then manage versions and overrides from the dashboard without redeploying:The prompts list page shows each prompt with its current version, model, override status, and a usage sparkline over the last 24 hours.
From the prompt detail page you can:
prompt.resolve()is called.AI span inspectors
Every AI SDK operation now gets a custom inspector in the run trace view:
ai.generateText/ai.streamText— Shows model, token usage, cost, the full message thread (system prompt, user message, assistant response), and linked prompt detailsai.generateObject/ai.streamObject— Same as above plus the JSON schema and structured outputai.toolCall— Shows tool name, call ID, and input argumentsai.embed— Shows model and the text being embeddedFor generation spans linked to a prompt, a "Prompt" tab shows the prompt metadata, the input variables passed to
resolve(), and the template content from the prompt version.All AI span inspectors include a compact timestamp and duration header.
Other improvements
@window-splitter/stateto fix snapshot restoration)<div>inside<p>DOM nesting warnings in span titles and chat messagesScreenshots