Skip to content

Commit b6da034

Browse files
claude[bot]Trigger.dev RepoOps
authored andcommitted
feat: add copy AI prompt button to org projects settings page
Adds a copy-AI-prompt button to the organization Projects settings page, shown when projects need a runtime upgrade, so the prompt can be pasted into a coding agent to perform the upgrade. Mono-RevId: e27230fc359ffb8e183382201d8da306eee825b4
1 parent 6ff343a commit b6da034

4 files changed

Lines changed: 127 additions & 40 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Projects that need a Node.js runtime update can now be handed to a coding agent: the organization Projects settings page has a button that copies a ready-to-paste prompt listing every project to update.

apps/webapp/app/components/SetupCommands.tsx

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import { CheckIcon } from "@heroicons/react/20/solid";
2-
import { createContext, useContext, useMemo, useRef, useState } from "react";
1+
import { createContext, useContext, useMemo, useState } from "react";
32
import { useAppOrigin } from "~/hooks/useAppOrigin";
43
import { useProject } from "~/hooks/useProject";
54
import { useTriggerCliTag } from "~/hooks/useTriggerCliTag";
6-
import { Button } from "./primitives/Buttons";
75
import {
86
ClientTabs,
97
ClientTabsContent,
108
ClientTabsList,
119
ClientTabsTrigger,
1210
} from "./primitives/ClientTabs";
1311
import { ClipboardField } from "./primitives/ClipboardField";
12+
import { CopyAgentPromptButton } from "./primitives/CopyButton";
1413
import { Header3 } from "./primitives/Headers";
15-
import { SimpleTooltip } from "./primitives/Tooltip";
1614

1715
type PackageManagerContextType = {
1816
activePackageManager: string;
@@ -165,43 +163,12 @@ export function InitAgentPromptV3() {
165163
const project = useProject();
166164
const apiUrl = useApiUrl();
167165
const cliTag = useTriggerCliTag();
168-
const [copied, setCopied] = useState(false);
169-
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
170-
171-
const onCopy = () => {
172-
const prompt = buildAgentSetupPrompt({
173-
projectRef: project.externalRef,
174-
apiUrl,
175-
cliTag,
176-
});
177-
setCopied(true);
178-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
179-
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
180-
void navigator.clipboard.writeText(prompt).catch(() => {});
181-
};
182-
183-
// The idle label is the longest, so reserve its width to stop the button from
184-
// resizing when it briefly swaps to the shorter "Copied prompt".
185-
const idleLabel = "Copy AI agent prompt";
186166

187167
return (
188-
<SimpleTooltip
189-
asChild
190-
tabbable
191-
button={
192-
<Button type="button" variant="primary/medium" onClick={onCopy}>
193-
<span className="grid justify-items-center">
194-
<span className="col-start-1 row-start-1 flex items-center gap-x-1.5">
195-
{copied && <CheckIcon className="size-4 shrink-0 text-text-bright" />}
196-
<span>{copied ? "Copied prompt" : idleLabel}</span>
197-
</span>
198-
<span aria-hidden className="invisible col-start-1 row-start-1">
199-
{idleLabel}
200-
</span>
201-
</span>
202-
</Button>
203-
}
204-
content="Copies a setup prompt to paste into Claude Code, Cursor, or any coding agent"
168+
<CopyAgentPromptButton
169+
prompt={buildAgentSetupPrompt({ projectRef: project.externalRef, apiUrl, cliTag })}
170+
label="Copy AI agent prompt"
171+
tooltip="Copies a setup prompt to paste into Claude Code, Cursor, or any coding agent"
205172
/>
206173
);
207174
}

apps/webapp/app/components/primitives/CopyButton.tsx

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { CheckIcon } from "@heroicons/react/20/solid";
12
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
3+
import { useEffect, useRef, useState } from "react";
24
import { useCopy } from "~/hooks/useCopy";
35
import { cn } from "~/utils/cn";
4-
import { Button } from "./Buttons";
6+
import { Button, type ButtonVariant } from "./Buttons";
57
import { SimpleTooltip } from "./Tooltip";
68

79
const sizes = {
@@ -117,3 +119,75 @@ export function CopyButton({
117119
</span>
118120
);
119121
}
122+
123+
/**
124+
* Copies a ready-to-paste AI agent prompt, briefly swapping its label to confirm.
125+
*
126+
* The prompt is built by the caller — it is long, page-specific text, not a value
127+
* a user would ever want echoed in a tooltip, so this stays a plain button rather
128+
* than reusing the `CopyButton` clipboard treatment above.
129+
*/
130+
export function CopyAgentPromptButton({
131+
prompt,
132+
label,
133+
tooltip,
134+
variant = "primary/medium",
135+
}: {
136+
prompt: string;
137+
/** Idle label. Keep it at least as long as "Copied prompt" — its width is reserved for it. */
138+
label: string;
139+
tooltip: string;
140+
variant?: ButtonVariant;
141+
}) {
142+
const [copied, setCopied] = useState(false);
143+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
144+
const unmountedRef = useRef(false);
145+
146+
useEffect(
147+
() => () => {
148+
unmountedRef.current = true;
149+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
150+
},
151+
[]
152+
);
153+
154+
const onCopy = async () => {
155+
try {
156+
// Throws when the clipboard API is unavailable (e.g. an insecure context) and
157+
// rejects when the write is denied. Either way the prompt was not copied, so
158+
// fall through without confirming it.
159+
await navigator.clipboard.writeText(prompt);
160+
} catch {
161+
return;
162+
}
163+
164+
if (unmountedRef.current) return;
165+
166+
setCopied(true);
167+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
168+
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
169+
};
170+
171+
return (
172+
<SimpleTooltip
173+
asChild
174+
tabbable
175+
button={
176+
<Button type="button" variant={variant} onClick={() => void onCopy()}>
177+
<span className="grid justify-items-center">
178+
<span className="col-start-1 row-start-1 flex items-center gap-x-1.5">
179+
{copied && <CheckIcon className="size-4 shrink-0 text-text-bright" />}
180+
<span>{copied ? "Copied prompt" : label}</span>
181+
</span>
182+
{/* The idle label is the longest, so reserve its width to stop the button from
183+
resizing when it briefly swaps to the shorter "Copied prompt". */}
184+
<span aria-hidden className="invisible col-start-1 row-start-1">
185+
{label}
186+
</span>
187+
</span>
188+
</Button>
189+
}
190+
content={tooltip}
191+
/>
192+
);
193+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.projects/ProjectsPage.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { InlineCode } from "~/components/code/InlineCode";
55
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
66
import { LinkButton } from "~/components/primitives/Buttons";
77
import { ClipboardField } from "~/components/primitives/ClipboardField";
8+
import { CopyAgentPromptButton } from "~/components/primitives/CopyButton";
89
import { DateTime } from "~/components/primitives/DateTime";
910
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
1011
import { Paragraph } from "~/components/primitives/Paragraph";
@@ -29,6 +30,29 @@ const CONFIG_SNIPPET = `export default defineConfig({
2930

3031
const CLI_COMMAND = "npx trigger.dev@latest projects list --needs-update";
3132

33+
function buildRuntimeUpdatePrompt({
34+
projects,
35+
targetMajor,
36+
}: {
37+
projects: ProjectRuntimeRow[];
38+
targetMajor: number;
39+
}) {
40+
const projectList = projects.map((project) => `- ${project.name} (${project.ref})`).join("\n");
41+
42+
return `Update these Trigger.dev projects to Node.js ${targetMajor}.
43+
44+
They still deploy on Node.js ${NODE_RUNTIME_UPDATE_MAJOR} in production. Moving to Node.js ${targetMajor} is a one-field change in each project's trigger.config.ts.
45+
46+
Projects to update:
47+
${projectList}
48+
49+
How to do it:
50+
1. Find the trigger.config.ts for each project above — the reference in parentheses is its "project" field.
51+
2. In defineConfig, set runtime: "node-${targetMajor}", adding the field if it isn't there. Reference: https://trigger.dev/docs/config/config-file#runtime
52+
3. Deploy each project so the new runtime takes effect.
53+
4. Check that nothing is left by running: ${CLI_COMMAND}`;
54+
}
55+
3256
/** A project and its current Production deployment, or `null` when it has never been deployed. */
3357
export type ProjectRuntimeRow = {
3458
name: string;
@@ -116,6 +140,22 @@ export function ProjectsPage({
116140
/>
117141
}
118142
/>
143+
144+
<SettingsRow
145+
title="Or let your AI agent do it"
146+
description="Copy a ready-to-paste prompt for Claude Code, Cursor, or any coding agent. It includes every project that needs updating."
147+
action={
148+
<CopyAgentPromptButton
149+
prompt={buildRuntimeUpdatePrompt({
150+
projects: needsUpdate,
151+
targetMajor: NODE_RUNTIME_TARGET_MAJOR,
152+
})}
153+
label="Copy AI agent prompt"
154+
tooltip="Copies an update prompt to paste into Claude Code, Cursor, or any coding agent"
155+
variant="primary/small"
156+
/>
157+
}
158+
/>
119159
</SettingsSection>
120160

121161
<SettingsSection>

0 commit comments

Comments
 (0)