54d95ee4b9
- Full prompt management UI: list, detail, override, and version
management for AI prompts defined with `prompts.define()`
- Rich AI span inspectors for all AI SDK operations with token usage,
messages, and prompt context
- Real-time generation tracking with live polling and filtering
## Prompt management
Define prompts in your code with `prompts.define()`, then manage
versions and overrides from the dashboard without redeploying:
```typescript
import { task, prompts } from "@trigger.dev/sdk";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const supportPrompt = prompts.define({
id: "customer-support",
model: "gpt-4o",
variables: z.object({
customerName: z.string(),
plan: z.string(),
issue: z.string(),
}),
content: `You are a support agent for Acme SaaS.
Customer: {{customerName}} ({{plan}} plan)
Issue: {{issue}}
Respond with empathy and precision.`,
});
export const supportTask = task({
id: "handle-support",
run: async (payload) => {
const resolved = await supportPrompt.resolve({
customerName: payload.name,
plan: payload.plan,
issue: payload.issue,
});
const result = await generateText({
model: openai(resolved.model ?? "gpt-4o"),
system: resolved.text,
prompt: payload.issue,
...resolved.toAISDKTelemetry(),
});
return { response: result.text };
},
});
```
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:
- **Create overrides** to change the prompt template or model without
redeploying. Overrides take priority over the deployed version when
`prompt.resolve()` is called.
- **Promote** any code-deployed version to be the current version
- **Browse generations** across all versions with infinite scroll and
live polling for new results
- **Filter** by version, model, operation type, and provider
- **View metrics** (total generations, avg tokens, avg cost, latency)
broken down by version
## 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 details
- **`ai.generateObject` / `ai.streamObject`** — Same as above plus the
JSON schema and structured output
- **`ai.toolCall`** — Shows tool name, call ID, and input arguments
- **`ai.embed`** — Shows model and the text being embedded
For 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
- Resizable panel sizes now persist across page refreshes (patched
`@window-splitter/state` to fix snapshot restoration)
- Run page panels also persist their sizes
- Fixed `<div>` inside `<p>` DOM nesting warnings in span titles and
chat messages
- Added Operations and Providers filters to the AI metrics dashboard
## Screenshots
<img width="3680" height="2392" alt="CleanShot 2026-03-21 at 10 14
17@2x"
src="https://github.com/user-attachments/assets/f3e59989-a2fa-4990-a9d0-3cacda431868"
/>
<img width="3680" height="2392" alt="CleanShot 2026-03-21 at 10 15
37@2x"
src="https://github.com/user-attachments/assets/2f2d02df-2d2b-44fb-ac6f-9153f6a6c387"
/>
<img width="3680" height="2392" alt="CleanShot 2026-03-21 at 10 15
54@2x"
src="https://github.com/user-attachments/assets/baa161e0-ef91-4fa4-a55f-986b71cccdf0"
/>
126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
import type { ViewUpdate } from "@codemirror/view";
|
|
import { EditorView, lineNumbers } from "@codemirror/view";
|
|
import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid";
|
|
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
|
|
import { useCodeMirror } from "@uiw/react-codemirror";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { cn } from "~/utils/cn";
|
|
import { Button } from "../primitives/Buttons";
|
|
import { getEditorSetup } from "./codeMirrorSetup";
|
|
import { darkTheme } from "./codeMirrorTheme";
|
|
|
|
export interface TextEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
|
defaultValue?: string;
|
|
readOnly?: boolean;
|
|
onChange?: (value: string) => void;
|
|
onUpdate?: (update: ViewUpdate) => void;
|
|
showCopyButton?: boolean;
|
|
additionalActions?: React.ReactNode;
|
|
}
|
|
|
|
export function TextEditor(opts: TextEditorProps) {
|
|
const {
|
|
defaultValue = "",
|
|
readOnly = false,
|
|
onChange,
|
|
onUpdate,
|
|
autoFocus,
|
|
showCopyButton = true,
|
|
additionalActions,
|
|
} = opts;
|
|
|
|
// Don't use default line numbers from setup — add our own with proper sizing
|
|
const extensions = getEditorSetup(false);
|
|
extensions.push(EditorView.lineWrapping);
|
|
extensions.push(
|
|
lineNumbers({
|
|
formatNumber: (n) => String(n),
|
|
})
|
|
);
|
|
extensions.push(
|
|
EditorView.theme({
|
|
".cm-lineNumbers": {
|
|
minWidth: "40px",
|
|
},
|
|
})
|
|
);
|
|
|
|
const editor = useRef<HTMLDivElement>(null);
|
|
const settings: Omit<UseCodeMirror, "onBlur"> = {
|
|
...opts,
|
|
container: editor.current,
|
|
extensions,
|
|
editable: !readOnly,
|
|
contentEditable: !readOnly,
|
|
value: defaultValue,
|
|
autoFocus,
|
|
theme: darkTheme(),
|
|
indentWithTab: false,
|
|
basicSetup: false,
|
|
onChange,
|
|
onUpdate,
|
|
};
|
|
const { setContainer, view } = useCodeMirror(settings);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (editor.current) {
|
|
setContainer(editor.current);
|
|
}
|
|
}, [setContainer]);
|
|
|
|
useEffect(() => {
|
|
if (view !== undefined) {
|
|
if (view.state.doc.toString() === defaultValue) return;
|
|
view.dispatch({
|
|
changes: { from: 0, to: view.state.doc.length, insert: defaultValue },
|
|
});
|
|
}
|
|
}, [defaultValue, view]);
|
|
|
|
const copy = useCallback(() => {
|
|
if (view === undefined) return;
|
|
navigator.clipboard.writeText(view.state.doc.toString());
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1500);
|
|
}, [view]);
|
|
|
|
const showToolbar = showCopyButton || additionalActions;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"grid",
|
|
showToolbar ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]",
|
|
opts.className
|
|
)}
|
|
>
|
|
{showToolbar && (
|
|
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
|
<div className="flex items-center">{additionalActions}</div>
|
|
<div className="flex items-center gap-2">
|
|
{showCopyButton && (
|
|
<Button
|
|
type="button"
|
|
variant="minimal/small"
|
|
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
|
|
trailingIconClassName={
|
|
copied ? "text-green-500 group-hover:text-green-500" : undefined
|
|
}
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
copy();
|
|
}}
|
|
>
|
|
Copy
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="min-h-0 min-w-0 overflow-auto" ref={editor} />
|
|
</div>
|
|
);
|
|
}
|