Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/dev-onboarding-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"trigger.dev": patch
---

The dev environment onboarding now tracks real progress. After you run `init`, the setup checklist marks your project as initialized, and it updates live as your dev server connects and your tasks register. The blank state also adds a "Copy AI agent prompt" button that copies a ready-to-paste setup prompt (pre-filled with your project reference) for Claude Code, Cursor, or any coding agent.

The `init` scaffold now imports from `@trigger.dev/sdk` instead of the deprecated `@trigger.dev/sdk/v3` subpath.
90 changes: 75 additions & 15 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
QuestionMarkCircleIcon,
RectangleGroupIcon,
RectangleStackIcon,
SparklesIcon,
Squares2X2Icon,
} from "@heroicons/react/20/solid";
import { useLocation } from "react-use";
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
} from "~/utils/pathBuilder";
import { AskAI } from "./AskAI";
import { CodeBlock } from "./code/CodeBlock";
import { useDevPresence } from "./DevPresence";
import { InlineCode } from "./code/InlineCode";
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
import { Feedback } from "./Feedback";
Expand All @@ -54,6 +56,7 @@ import { StepNumber } from "./primitives/StepNumber";
import { TextLink } from "./primitives/TextLink";
import { SimpleTooltip } from "./primitives/Tooltip";
import {
InitAgentPromptV3,
InitCommandV3,
PackageManagerProvider,
TriggerDeployStep,
Expand All @@ -62,12 +65,16 @@ import {
import { StepContentContainer } from "./StepContentContainer";
import { V4Badge } from "./V4Badge";

export function HasNoTasksDev() {
export function HasNoTasksDev({ initializedAt }: { initializedAt: Date | string | null }) {
const { isConnected } = useDevPresence();
const initialized = !!initializedAt;
const devConnected = isConnected === true;

return (
<PackageManagerProvider>
<div>
<div className="mb-6 flex items-center justify-between border-b">
<Header1 spacing>Get setup in 3 minutes</Header1>
<Header1 spacing>Get set up in 2 minutes</Header1>
<div className="flex items-center gap-2">
<Feedback
button={
Expand All @@ -79,22 +86,75 @@ export function HasNoTasksDev() {
/>
</div>
</div>
<StepNumber stepNumber="1" title="Run the CLI 'init' command in an existing project" />
<StepContentContainer>
<InitCommandV3 />
<Paragraph spacing>
You'll notice a new folder in your project called{" "}
<InlineCode variant="small">trigger</InlineCode>. We've added a few simple example tasks
in there to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run the CLI 'dev' command" />
{!initialized && (
<>
<div className="flex flex-col gap-4 rounded-md border border-indigo-400/20 bg-indigo-800/10 p-4 sm:flex-row sm:items-center">
<span className="flex size-9 shrink-0 items-center justify-center rounded-md bg-indigo-500/15 text-indigo-400">
<SparklesIcon className="size-5" />
</span>
<div className="min-w-0 flex-1">
<Paragraph className="text-text-bright">Set it up with your AI agent</Paragraph>
<Paragraph variant="small" className="text-text-dimmed">
Copy a ready-to-paste prompt for Claude Code, Cursor, or any coding agent. It
includes your project reference.
</Paragraph>
</div>
<div className="shrink-0">
<InitAgentPromptV3 />
</div>
</div>
<div className="my-6 flex items-center gap-3">
<div className="h-px flex-1 bg-grid-bright" />
<span className="text-xs uppercase tracking-wide text-text-dimmed">
or set it up yourself
</span>
<div className="h-px flex-1 bg-grid-bright" />
</div>
</>
)}
<StepNumber
stepNumber="1"
title={initialized ? "Project initialized" : "Initialize your project"}
complete={initialized}
/>
<StepContentContainer>
<TriggerDevStepV3 />
{initialized ? (
<Paragraph>
Your project is initialized. Your tasks live in the{" "}
<InlineCode variant="small">trigger</InlineCode> directory.
</Paragraph>
) : (
<>
<InitCommandV3 />
<Paragraph spacing>
Run this in an existing project. You'll notice a new folder called{" "}
<InlineCode variant="small">trigger</InlineCode> with a few example tasks to help
you get started.
</Paragraph>
</>
)}
</StepContentContainer>
<StepNumber stepNumber="3" title="Waiting for tasks" displaySpinner />
<StepNumber
stepNumber="2"
title={devConnected ? "Dev server connected" : "Start the dev server"}
complete={devConnected}
displaySpinner={!devConnected}
/>
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
{devConnected ? (
<Paragraph>
Your dev server is connected. Your tasks will appear here automatically as soon as
they register.
</Paragraph>
) : (
<>
<TriggerDevStepV3 />
<Paragraph spacing>
Keep this running while you develop. Once your tasks register, this page updates
automatically.
</Paragraph>
</>
)}
</StepContentContainer>
</div>
</PackageManagerProvider>
Expand Down
104 changes: 92 additions & 12 deletions apps/webapp/app/components/SetupCommands.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createContext, useContext, useState } from "react";
import { CheckIcon, SparklesIcon } from "@heroicons/react/20/solid";
import { createContext, useContext, useRef, useState } from "react";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useProject } from "~/hooks/useProject";
import { useTriggerCliTag } from "~/hooks/useTriggerCliTag";
import { Button } from "./primitives/Buttons";
import {
ClientTabs,
ClientTabsContent,
Expand All @@ -10,6 +12,7 @@ import {
} from "./primitives/ClientTabs";
import { ClipboardField } from "./primitives/ClipboardField";
import { Header3 } from "./primitives/Headers";
import { SimpleTooltip } from "./primitives/Tooltip";

type PackageManagerContextType = {
activePackageManager: string;
Expand All @@ -36,26 +39,23 @@ function usePackageManager() {
return context;
}

function getApiUrlArg() {
function useApiUrl() {
const appOrigin = useAppOrigin();

let apiUrl: string | undefined = undefined;

switch (appOrigin) {
case "https://cloud.trigger.dev":
// don't display the arg, use the CLI default
break;
return undefined;
case "https://test-cloud.trigger.dev":
apiUrl = "https://test-api.trigger.dev";
break;
return "https://test-api.trigger.dev";
case "https://internal.trigger.dev":
apiUrl = "https://internal-api.trigger.dev";
break;
return "https://internal-api.trigger.dev";
default:
apiUrl = appOrigin;
break;
return appOrigin;
}
}

function getApiUrlArg() {
const apiUrl = useApiUrl();
return apiUrl ? `-a ${apiUrl}` : undefined;
}

Expand Down Expand Up @@ -117,6 +117,86 @@ export function InitCommandV3({ title }: TabsProps) {
);
}

function buildAgentSetupPrompt({
projectRef,
apiUrl,
cliTag,
}: {
projectRef: string;
apiUrl: string | undefined;
cliTag: string;
}) {
const apiUrlArg = apiUrl ? ` -a ${apiUrl}` : "";
const apiUrlLine = apiUrl ? `\nTrigger.dev API URL: ${apiUrl}` : "";

return `Set up Trigger.dev in this project.

Trigger.dev runs your background tasks. This is an existing codebase — add Trigger.dev to it and get one task running in the development environment.

Project reference: ${projectRef}${apiUrlLine}

How to do it:
1. If you have the Trigger.dev MCP server available, use its "initialize_project" tool with the project reference above.
2. Otherwise run this and follow its output:
npx trigger.dev@${cliTag} init -p ${projectRef}${apiUrlArg}
3. If you set it up by hand, follow https://trigger.dev/docs/manual-setup and make sure you end up with:
Comment thread
ericallam marked this conversation as resolved.
- "@trigger.dev/sdk" installed (latest) and "@trigger.dev/build" as a dev dependency
- a trigger.config.ts with: import { defineConfig } from "@trigger.dev/sdk", project: "${projectRef}", dirs: ["./src/trigger"], and a maxDuration
- a src/trigger/ directory with at least one exported task created with task() from "@trigger.dev/sdk"
- trigger.config.ts added to tsconfig "include", and ".trigger" added to .gitignore

Golden rules:
- Import from "@trigger.dev/sdk". Never "@trigger.dev/sdk/v3" or the deprecated client.defineJob.
- Export every task, including subtasks.
- Use the built-in fetch, not node-fetch.
- Never wrap wait.*, triggerAndWait, or batchTriggerAndWait in Promise.all.

Two steps I have to do myself — ask me when you need them:
- Running "npx trigger.dev@${cliTag} login" (it opens a browser).
- Giving you the development TRIGGER_SECRET_KEY from the dashboard to put in .env.

When you're done, run "npx trigger.dev@${cliTag} dev" and confirm the task shows up in the Trigger.dev dashboard.`;
}

export function InitAgentPromptV3() {
const project = useProject();
const apiUrl = useApiUrl();
const cliTag = useTriggerCliTag();
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const onCopy = () => {
const prompt = buildAgentSetupPrompt({
projectRef: project.externalRef,
apiUrl,
cliTag,
});
setCopied(true);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
void navigator.clipboard.writeText(prompt).catch(() => {});
};

return (
<SimpleTooltip
asChild
tabbable
button={
<Button
type="button"
variant="primary/medium"
LeadingIcon={copied ? CheckIcon : SparklesIcon}
leadingIconClassName={copied ? "text-success" : undefined}
onClick={onCopy}
>
{copied ? "Copied prompt" : "Copy AI agent prompt"}
</Button>
}
content="Copies a setup prompt to paste into Claude Code, Cursor, or any coding agent"
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}

export function TriggerDevStepV3({ title }: TabsProps) {
const triggerCliTag = useTriggerCliTag();
const { activePackageManager, setActivePackageManager } = usePackageManager();
Expand Down
4 changes: 4 additions & 0 deletions apps/webapp/app/presenters/v3/TasksStreamPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ export class TasksStreamPresenter {
safeSend({ data: message.createdAt.toISOString() });
});

subscriber.on("PROJECT_INITIALIZED", async (message) => {
safeSend({ data: message.initializedAt.toISOString() });
});

pinger = setInterval(() => {
if (signal.aborted) {
return close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useSearchParams } from "~/hooks/useSearchParam";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { prisma } from "~/db.server";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import {
Expand Down Expand Up @@ -128,7 +129,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {

const usefulLinksPreference = await getUsefulLinksPreference(request);

return typeddefer({ items, hourlyActivity, runningStates, usefulLinksPreference });
const initialized = await prisma.project.findFirst({
where: { id: project.id },
select: { initializedAt: true },
});

return typeddefer({
items,
hourlyActivity,
runningStates,
usefulLinksPreference,
projectInitializedAt: initialized?.initializedAt ?? null,
});
Comment thread
ericallam marked this conversation as resolved.
} catch (error) {
console.error(error);
throw new Response(undefined, {
Expand Down Expand Up @@ -195,7 +207,7 @@ export default function Page() {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const { items, hourlyActivity, runningStates, usefulLinksPreference } =
const { items, hourlyActivity, runningStates, usefulLinksPreference, projectInitializedAt } =
useTypedLoaderData<typeof loader>();
const { value, values } = useSearchParams();

Expand Down Expand Up @@ -355,7 +367,7 @@ export default function Page() {
</div>
) : environment.type === "DEVELOPMENT" ? (
<MainCenteredContainer className="max-w-prose">
<HasNoTasksDev />
<HasNoTasksDev initializedAt={projectInitializedAt} />
</MainCenteredContainer>
) : (
<MainCenteredContainer className="max-w-prose">
Expand Down
Loading
Loading