Skip to content

Commit 4f20fcc

Browse files
committed
feat(webapp,cli,database): track real dev onboarding progress
The dev environment "Get set up" panel now reflects actual progress instead of static instructions. Running trigger init records the project as initialized (via a new project-scoped endpoint the CLI calls), so step 1 ticks off, and the panel updates live as the dev server connects and tasks register. Adds a "Copy AI agent prompt" button that copies a ready-to-paste setup prompt pre-filled with the project reference, and updates the init scaffold to import from @trigger.dev/sdk instead of the deprecated /v3 subpath.
1 parent 820c079 commit 4f20fcc

14 files changed

Lines changed: 264 additions & 32 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
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.
6+
7+
The `init` scaffold now imports from `@trigger.dev/sdk` instead of the deprecated `@trigger.dev/sdk/v3` subpath.

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
QuestionMarkCircleIcon,
99
RectangleGroupIcon,
1010
RectangleStackIcon,
11+
SparklesIcon,
1112
Squares2X2Icon,
1213
} from "@heroicons/react/20/solid";
1314
import { useLocation } from "react-use";
@@ -36,6 +37,7 @@ import {
3637
} from "~/utils/pathBuilder";
3738
import { AskAI } from "./AskAI";
3839
import { CodeBlock } from "./code/CodeBlock";
40+
import { useDevPresence } from "./DevPresence";
3941
import { InlineCode } from "./code/InlineCode";
4042
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
4143
import { Feedback } from "./Feedback";
@@ -54,6 +56,7 @@ import { StepNumber } from "./primitives/StepNumber";
5456
import { TextLink } from "./primitives/TextLink";
5557
import { SimpleTooltip } from "./primitives/Tooltip";
5658
import {
59+
InitAgentPromptV3,
5760
InitCommandV3,
5861
PackageManagerProvider,
5962
TriggerDeployStep,
@@ -63,11 +66,16 @@ import { StepContentContainer } from "./StepContentContainer";
6366
import { V4Badge } from "./V4Badge";
6467

6568
export function HasNoTasksDev() {
69+
const project = useProject();
70+
const { isConnected } = useDevPresence();
71+
const initialized = !!project.initializedAt;
72+
const devConnected = isConnected === true;
73+
6674
return (
6775
<PackageManagerProvider>
6876
<div>
6977
<div className="mb-6 flex items-center justify-between border-b">
70-
<Header1 spacing>Get setup in 3 minutes</Header1>
78+
<Header1 spacing>Get set up in 2 minutes</Header1>
7179
<div className="flex items-center gap-2">
7280
<Feedback
7381
button={
@@ -79,22 +87,75 @@ export function HasNoTasksDev() {
7987
/>
8088
</div>
8189
</div>
82-
<StepNumber stepNumber="1" title="Run the CLI 'init' command in an existing project" />
83-
<StepContentContainer>
84-
<InitCommandV3 />
85-
<Paragraph spacing>
86-
You'll notice a new folder in your project called{" "}
87-
<InlineCode variant="small">trigger</InlineCode>. We've added a few simple example tasks
88-
in there to help you get started.
89-
</Paragraph>
90-
</StepContentContainer>
91-
<StepNumber stepNumber="2" title="Run the CLI 'dev' command" />
90+
{!initialized && (
91+
<>
92+
<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">
93+
<span className="flex size-9 shrink-0 items-center justify-center rounded-md bg-indigo-500/15 text-indigo-400">
94+
<SparklesIcon className="size-5" />
95+
</span>
96+
<div className="min-w-0 flex-1">
97+
<Paragraph className="text-text-bright">Set it up with your AI agent</Paragraph>
98+
<Paragraph variant="small" className="text-text-dimmed">
99+
Copy a ready-to-paste prompt for Claude Code, Cursor, or any coding agent. It
100+
includes your project reference.
101+
</Paragraph>
102+
</div>
103+
<div className="shrink-0">
104+
<InitAgentPromptV3 />
105+
</div>
106+
</div>
107+
<div className="my-6 flex items-center gap-3">
108+
<div className="h-px flex-1 bg-grid-bright" />
109+
<span className="text-xs uppercase tracking-wide text-text-dimmed">
110+
or set it up yourself
111+
</span>
112+
<div className="h-px flex-1 bg-grid-bright" />
113+
</div>
114+
</>
115+
)}
116+
<StepNumber
117+
stepNumber="1"
118+
title={initialized ? "Project initialized" : "Initialize your project"}
119+
complete={initialized}
120+
/>
92121
<StepContentContainer>
93-
<TriggerDevStepV3 />
122+
{initialized ? (
123+
<Paragraph>
124+
Your project is initialized. Your tasks live in the{" "}
125+
<InlineCode variant="small">trigger</InlineCode> directory.
126+
</Paragraph>
127+
) : (
128+
<>
129+
<InitCommandV3 />
130+
<Paragraph spacing>
131+
Run this in an existing project. You'll notice a new folder called{" "}
132+
<InlineCode variant="small">trigger</InlineCode> with a few example tasks to help
133+
you get started.
134+
</Paragraph>
135+
</>
136+
)}
94137
</StepContentContainer>
95-
<StepNumber stepNumber="3" title="Waiting for tasks" displaySpinner />
138+
<StepNumber
139+
stepNumber="2"
140+
title={devConnected ? "Dev server connected" : "Start the dev server"}
141+
complete={devConnected}
142+
displaySpinner={!devConnected}
143+
/>
96144
<StepContentContainer>
97-
<Paragraph>This page will automatically refresh.</Paragraph>
145+
{devConnected ? (
146+
<Paragraph>
147+
Your dev server is connected. Your tasks will appear here automatically as soon as
148+
they register.
149+
</Paragraph>
150+
) : (
151+
<>
152+
<TriggerDevStepV3 />
153+
<Paragraph spacing>
154+
Keep this running while you develop. Once your tasks register, this page updates
155+
automatically.
156+
</Paragraph>
157+
</>
158+
)}
98159
</StepContentContainer>
99160
</div>
100161
</PackageManagerProvider>

apps/webapp/app/components/SetupCommands.tsx

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { createContext, useContext, useState } from "react";
1+
import { CheckIcon, SparklesIcon } from "@heroicons/react/20/solid";
2+
import { createContext, useContext, useRef, useState } from "react";
23
import { useAppOrigin } from "~/hooks/useAppOrigin";
34
import { useProject } from "~/hooks/useProject";
45
import { useTriggerCliTag } from "~/hooks/useTriggerCliTag";
6+
import { Button } from "./primitives/Buttons";
57
import {
68
ClientTabs,
79
ClientTabsContent,
@@ -10,6 +12,7 @@ import {
1012
} from "./primitives/ClientTabs";
1113
import { ClipboardField } from "./primitives/ClipboardField";
1214
import { Header3 } from "./primitives/Headers";
15+
import { SimpleTooltip } from "./primitives/Tooltip";
1316

1417
type PackageManagerContextType = {
1518
activePackageManager: string;
@@ -36,26 +39,23 @@ function usePackageManager() {
3639
return context;
3740
}
3841

39-
function getApiUrlArg() {
42+
function useApiUrl() {
4043
const appOrigin = useAppOrigin();
4144

42-
let apiUrl: string | undefined = undefined;
43-
4445
switch (appOrigin) {
4546
case "https://cloud.trigger.dev":
46-
// don't display the arg, use the CLI default
47-
break;
47+
return undefined;
4848
case "https://test-cloud.trigger.dev":
49-
apiUrl = "https://test-api.trigger.dev";
50-
break;
49+
return "https://test-api.trigger.dev";
5150
case "https://internal.trigger.dev":
52-
apiUrl = "https://internal-api.trigger.dev";
53-
break;
51+
return "https://internal-api.trigger.dev";
5452
default:
55-
apiUrl = appOrigin;
56-
break;
53+
return appOrigin;
5754
}
55+
}
5856

57+
function getApiUrlArg() {
58+
const apiUrl = useApiUrl();
5959
return apiUrl ? `-a ${apiUrl}` : undefined;
6060
}
6161

@@ -117,6 +117,86 @@ export function InitCommandV3({ title }: TabsProps) {
117117
);
118118
}
119119

120+
function buildAgentSetupPrompt({
121+
projectRef,
122+
apiUrl,
123+
cliTag,
124+
}: {
125+
projectRef: string;
126+
apiUrl: string | undefined;
127+
cliTag: string;
128+
}) {
129+
const apiUrlArg = apiUrl ? ` -a ${apiUrl}` : "";
130+
const apiUrlLine = apiUrl ? `\nTrigger.dev API URL: ${apiUrl}` : "";
131+
132+
return `Set up Trigger.dev in this project.
133+
134+
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.
135+
136+
Project reference: ${projectRef}${apiUrlLine}
137+
138+
How to do it:
139+
1. If you have the Trigger.dev MCP server available, use its "initialize_project" tool with the project reference above.
140+
2. Otherwise run this and follow its output:
141+
npx trigger.dev@${cliTag} init -p ${projectRef}${apiUrlArg}
142+
3. If you set it up by hand, follow https://trigger.dev/docs/manual-setup and make sure you end up with:
143+
- "@trigger.dev/sdk" installed (latest) and "@trigger.dev/build" as a dev dependency
144+
- a trigger.config.ts with: import { defineConfig } from "@trigger.dev/sdk", project: "${projectRef}", dirs: ["./src/trigger"], and a maxDuration
145+
- a src/trigger/ directory with at least one exported task created with task() from "@trigger.dev/sdk"
146+
- trigger.config.ts added to tsconfig "include", and ".trigger" added to .gitignore
147+
148+
Golden rules:
149+
- Import from "@trigger.dev/sdk". Never "@trigger.dev/sdk/v3" or the deprecated client.defineJob.
150+
- Export every task, including subtasks.
151+
- Use the built-in fetch, not node-fetch.
152+
- Never wrap wait.*, triggerAndWait, or batchTriggerAndWait in Promise.all.
153+
154+
Two steps I have to do myself — ask me when you need them:
155+
- Running "npx trigger.dev@${cliTag} login" (it opens a browser).
156+
- Giving you the development TRIGGER_SECRET_KEY from the dashboard to put in .env.
157+
158+
When you're done, run "npx trigger.dev@${cliTag} dev" and confirm the task shows up in the Trigger.dev dashboard.`;
159+
}
160+
161+
export function InitAgentPromptV3() {
162+
const project = useProject();
163+
const apiUrl = useApiUrl();
164+
const cliTag = useTriggerCliTag();
165+
const [copied, setCopied] = useState(false);
166+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
167+
168+
const onCopy = () => {
169+
const prompt = buildAgentSetupPrompt({
170+
projectRef: project.externalRef,
171+
apiUrl,
172+
cliTag,
173+
});
174+
setCopied(true);
175+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
176+
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
177+
void navigator.clipboard.writeText(prompt).catch(() => {});
178+
};
179+
180+
return (
181+
<SimpleTooltip
182+
asChild
183+
tabbable
184+
button={
185+
<Button
186+
type="button"
187+
variant="primary/medium"
188+
LeadingIcon={copied ? CheckIcon : SparklesIcon}
189+
leadingIconClassName={copied ? "text-success" : undefined}
190+
onClick={onCopy}
191+
>
192+
{copied ? "Copied prompt" : "Copy AI agent prompt"}
193+
</Button>
194+
}
195+
content="Copies a setup prompt to paste into Claude Code, Cursor, or any coding agent"
196+
/>
197+
);
198+
}
199+
120200
export function TriggerDevStepV3({ title }: TabsProps) {
121201
const triggerCliTag = useTriggerCliTag();
122202
const { activePackageManager, setActivePackageManager } = usePackageManager();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { json } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { prisma } from "~/db.server";
4+
import { createActionPATApiRoute } from "~/services/routeBuilders/apiBuilder.server";
5+
6+
const ParamsSchema = z.object({
7+
projectRef: z.string(),
8+
});
9+
10+
export const action = createActionPATApiRoute(
11+
{
12+
method: "POST",
13+
params: ParamsSchema,
14+
},
15+
async ({ params, authentication }) => {
16+
const project = await prisma.project.findFirst({
17+
where: {
18+
externalRef: params.projectRef,
19+
organization: {
20+
deletedAt: null,
21+
members: { some: { userId: authentication.userId } },
22+
},
23+
deletedAt: null,
24+
},
25+
select: { id: true, initializedAt: true },
26+
});
27+
28+
if (!project) {
29+
return json({ error: "Project not found" }, { status: 404 });
30+
}
31+
32+
if (!project.initializedAt) {
33+
await prisma.project.updateMany({
34+
where: { id: project.id, initializedAt: null },
35+
data: { initializedAt: new Date() },
36+
});
37+
}
38+
39+
const { initializedAt } =
40+
(await prisma.project.findFirst({
41+
where: { id: project.id },
42+
select: { initializedAt: true },
43+
})) ?? {};
44+
45+
return json({ id: project.id, initializedAt: initializedAt ?? null });
46+
}
47+
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Project" ADD COLUMN "initializedAt" TIMESTAMP(3);

internal-packages/database/prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ model Project {
469469
updatedAt DateTime @updatedAt
470470
deletedAt DateTime?
471471
472+
/// Set the first time the CLI `init` command completes against this project. Drives the dev onboarding progress.
473+
initializedAt DateTime?
474+
472475
version ProjectVersion @default(V2)
473476
engine RunEngineVersion @default(V1)
474477

packages/cli-v3/src/apiClient.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ const CliPlatformNotificationResponseSchema = z.object({
9595
.nullable(),
9696
});
9797

98+
const MarkProjectInitializedResponseBody = z.object({
99+
id: z.string(),
100+
initializedAt: z.string().nullable(),
101+
});
102+
98103
export class CliApiClient {
99104
private engineURL: string;
100105
private source: "cli" | "mcp";
@@ -174,6 +179,24 @@ export class CliApiClient {
174179
});
175180
}
176181

182+
async markProjectInitialized(projectRef: string) {
183+
if (!this.accessToken) {
184+
throw new Error("markProjectInitialized: No access token");
185+
}
186+
187+
return wrapZodFetch(
188+
MarkProjectInitializedResponseBody,
189+
`${this.apiURL}/api/v1/projects/${projectRef}/init`,
190+
{
191+
method: "POST",
192+
headers: {
193+
Authorization: `Bearer ${this.accessToken}`,
194+
"Content-Type": "application/json",
195+
},
196+
}
197+
);
198+
}
199+
177200
async getProjects() {
178201
if (!this.accessToken) {
179202
throw new Error("getProjects: No access token");

packages/cli-v3/src/commands/init.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,15 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
339339
// Ignore .trigger dir
340340
await gitIgnoreDotTriggerDir(dir, options);
341341

342+
try {
343+
const result = await apiClient.markProjectInitialized(selectedProject.externalRef);
344+
if (!result.success) {
345+
logger.debug("Failed to mark project as initialized", { error: result.error });
346+
}
347+
} catch (error) {
348+
logger.debug("Failed to mark project as initialized", { error });
349+
}
350+
342351
const projectDashboard = cliLink(
343352
"project dashboard",
344353
`${authorization.dashboardUrl}/projects/v3/${selectedProject.externalRef}`

packages/cli-v3/templates/examples/schedule.mjs.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { logger, schedules, wait } from "@trigger.dev/sdk/v3";
1+
import { logger, schedules, wait } from "@trigger.dev/sdk";
22

33
export const firstScheduledTask = schedules.task({
44
id: "first-scheduled-task",

0 commit comments

Comments
 (0)