{result.error}
+
+ {logo}
+
+ {message}
+{question}
++ Next Story = I'll do one more, then ask again. Yolo = I'll finish everything without stopping! +
+ {error &&{error}
} +Loading PRD...
++ {isYolo ? `Starting all ${storyCount} stories...` : "Starting..."} +
+{question}
++ Check the PRD in the right panel. Start = I'll ask after each story. Yolo = I'll do everything! +
+ {error &&{error}
} +
+ {input}
+
+ )}
+ + Changes made locally (check GITHUB_TOKEN permissions) +
+ )} +Desktop required
++ Background Ralph needs a larger screen. Please use a desktop browser. +
+{ }
+PRD will appear here
+{isActive ? "" : ""}
+{isActive ? "Progress will appear as stories complete" : "No progress yet"}
++ Submit a task to get started +
+Error loading run: {runError.message}
+🍩
+Run canceled
+“Me fail English? That's unpossible!”
++ Run failed ({run.status}) +
+ {errorMessage && ( +
+ {errorMessage}
+
+ )}
+ {!errorMessage && lastStatus && lastStatus.type !== "error" && (
+ + Last action: {lastStatus.type} — {lastStatus.message?.slice(0, 100)} +
+ )} +Next steps:
+Run timed out
++ The task exceeded its maximum duration. +
+Run expired
++ The waitpoint timed out while waiting for your response (24 hours). +
++ Progress updates will appear here when you start the task +
+Error: {error.message}
+Processing complete!
+ )} + + {latestUpdate?.steps &&Steps
++ Interactive examples showing Trigger.dev streaming patterns. Click an + example to see live code alongside a working demo. +
+ +... output
+function ShikiLine({ html, lineIndex }: { html: string; lineIndex: number }) {
+ const codeMatch = html.match(/]*>([\s\S]*?)<\/code>/)
+ if (!codeMatch) {
+ return
+ }
+
+ const lines = codeMatch[1].split("\n")
+ const lineHtml = lines[lineIndex] || ""
+
+ return (
+
+ )
+}
diff --git a/trigger-realtime-demo/realtime-nextjs/src/components/split-viewer.tsx b/trigger-realtime-demo/realtime-nextjs/src/components/split-viewer.tsx
new file mode 100644
index 0000000..582e040
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/components/split-viewer.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import { ReactNode, createContext, useContext, useState } from "react"
+
+type HighlightState = {
+ file: string
+ lines: [number, number]
+} | null
+
+type SplitViewerContextType = {
+ highlight: HighlightState
+ setHighlight: (h: HighlightState) => void
+}
+
+const SplitViewerContext = createContext(null)
+
+export function useSplitViewer() {
+ const ctx = useContext(SplitViewerContext)
+ if (!ctx) throw new Error("useSplitViewer must be used within SplitViewer")
+ return ctx
+}
+
+type SplitViewerProps = {
+ appPanel: ReactNode
+ codePanel: ReactNode
+}
+
+export function SplitViewer({ appPanel, codePanel }: SplitViewerProps) {
+ const [highlight, setHighlight] = useState(null)
+
+ return (
+
+
+ {/* Left: App Panel */}
+
+ {appPanel}
+
+ {/* Right: Code Panel */}
+
+ {codePanel}
+
+
+
+ )
+}
diff --git a/trigger-realtime-demo/realtime-nextjs/src/components/ui/button.tsx b/trigger-realtime-demo/realtime-nextjs/src/components/ui/button.tsx
new file mode 100644
index 0000000..37a7d4b
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/components/ui/button.tsx
@@ -0,0 +1,62 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/trigger-realtime-demo/realtime-nextjs/src/components/ui/card.tsx b/trigger-realtime-demo/realtime-nextjs/src/components/ui/card.tsx
new file mode 100644
index 0000000..681ad98
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/trigger-realtime-demo/realtime-nextjs/src/components/ui/tabs.tsx b/trigger-realtime-demo/realtime-nextjs/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..497ba5e
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/trigger-realtime-demo/realtime-nextjs/src/lib/code-mappings.ts b/trigger-realtime-demo/realtime-nextjs/src/lib/code-mappings.ts
new file mode 100644
index 0000000..8e9f0b6
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/lib/code-mappings.ts
@@ -0,0 +1,36 @@
+// Maps UI elements to code locations for highlighting
+// Line numbers refer to the DISPLAY code in page.tsx, not actual source files
+
+export type CodeMapping = {
+ file: string
+ lines: [number, number]
+ description?: string
+}
+
+export const progressMappings: Record = {
+ "trigger-button": {
+ file: "actions.ts",
+ lines: [10, 15],
+ description: "tasks.trigger() starts a new run of the task",
+ },
+ "progress-stream": {
+ file: "streams.ts",
+ lines: [14, 16],
+ description: "streams.define() creates a typed stream for real-time data",
+ },
+ "stream-write": {
+ file: "task.ts",
+ lines: [21, 27],
+ description: "progressStream.append() sends progress to the frontend instantly",
+ },
+ "task-definition": {
+ file: "task.ts",
+ lines: [6, 11],
+ description: "Task config: id for triggering, maxDuration for timeout",
+ },
+ "public-token": {
+ file: "actions.ts",
+ lines: [17, 20],
+ description: "Public token scoped to read only this specific run",
+ },
+}
diff --git a/trigger-realtime-demo/realtime-nextjs/src/lib/shiki.ts b/trigger-realtime-demo/realtime-nextjs/src/lib/shiki.ts
new file mode 100644
index 0000000..10eb209
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/lib/shiki.ts
@@ -0,0 +1,21 @@
+import { createHighlighter, Highlighter } from "shiki"
+
+let highlighter: Highlighter | null = null
+
+export async function getHighlighter() {
+ if (!highlighter) {
+ highlighter = await createHighlighter({
+ themes: ["tokyo-night"],
+ langs: ["typescript", "tsx", "javascript", "json", "bash"],
+ })
+ }
+ return highlighter
+}
+
+export async function highlightCode(code: string, lang: string): Promise {
+ const h = await getHighlighter()
+ return h.codeToHtml(code, {
+ lang,
+ theme: "tokyo-night",
+ })
+}
diff --git a/trigger-realtime-demo/realtime-nextjs/src/lib/utils.ts b/trigger-realtime-demo/realtime-nextjs/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/trigger-realtime-demo/realtime-nextjs/src/trigger/streams.ts b/trigger-realtime-demo/realtime-nextjs/src/trigger/streams.ts
new file mode 100644
index 0000000..44375aa
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/trigger/streams.ts
@@ -0,0 +1,20 @@
+import { streams } from "@trigger.dev/sdk/v3"
+
+export type Step = {
+ id: string
+ label: string
+ description: string
+ status: "pending" | "active" | "completed"
+}
+
+export type ProgressUpdate = {
+ current: number
+ total: number
+ message: string
+ steps: Step[]
+ currentStepId: string
+}
+
+export const progressStream = streams.define({
+ id: "progress",
+})
diff --git a/trigger-realtime-demo/realtime-nextjs/src/trigger/tasks.ts b/trigger-realtime-demo/realtime-nextjs/src/trigger/tasks.ts
new file mode 100644
index 0000000..11b5b44
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/src/trigger/tasks.ts
@@ -0,0 +1,63 @@
+import { task } from "@trigger.dev/sdk/v3"
+import { progressStream, Step } from "./streams"
+
+function createInitialSteps(): Step[] {
+ return [
+ { id: "init", label: "Initialize", description: "Setting up the task", status: "pending" },
+ { id: "process", label: "Process", description: "Processing items", status: "pending" },
+ { id: "finalize", label: "Finalize", description: "Preparing results", status: "pending" },
+ ]
+}
+
+// Marks current step as active, previous as completed
+function updateSteps(steps: Step[], currentStepId: string): Step[] {
+ let foundCurrent = false
+ return steps.map((step) => {
+ if (step.id === currentStepId) {
+ foundCurrent = true
+ return { ...step, status: "active" as const }
+ }
+ return { ...step, status: foundCurrent ? ("pending" as const) : ("completed" as const) }
+ })
+}
+
+export const processDataTask = task({
+ id: "process-data",
+ maxDuration: 300,
+ run: async (payload: { items: number }) => {
+ const total = payload.items
+ let steps = createInitialSteps()
+
+ // Step 1: Initialize
+ steps = updateSteps(steps, "init")
+ await progressStream.append({
+ current: 0, total, message: "Initializing...", steps, currentStepId: "init",
+ })
+ await new Promise((r) => setTimeout(r, 500))
+
+ // Step 2: Process items
+ steps = updateSteps(steps, "process")
+ for (let i = 0; i < total; i++) {
+ await new Promise((r) => setTimeout(r, 500))
+ await progressStream.append({
+ current: i + 1, total, message: `Processing item ${i + 1} of ${total}...`,
+ steps, currentStepId: "process",
+ })
+ }
+
+ // Step 3: Finalize
+ steps = updateSteps(steps, "finalize")
+ await progressStream.append({
+ current: total, total, message: "Finalizing...", steps, currentStepId: "finalize",
+ })
+ await new Promise((r) => setTimeout(r, 300))
+
+ // Mark complete
+ steps = steps.map((s) => ({ ...s, status: "completed" as const }))
+ await progressStream.append({
+ current: total, total, message: "Complete!", steps, currentStepId: "finalize",
+ })
+
+ return { processed: total }
+ },
+})
diff --git a/trigger-realtime-demo/realtime-nextjs/trigger.config.ts b/trigger-realtime-demo/realtime-nextjs/trigger.config.ts
new file mode 100644
index 0000000..67aa54e
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/trigger.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "@trigger.dev/sdk/v3"
+
+export default defineConfig({
+ project: "proj_placeholder", // Replace with your project ref
+ runtime: "node",
+ logLevel: "info",
+ maxDuration: 300,
+ dirs: ["./src/trigger"],
+})
diff --git a/trigger-realtime-demo/realtime-nextjs/tsconfig.json b/trigger-realtime-demo/realtime-nextjs/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/trigger-realtime-demo/realtime-nextjs/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}