Skip to content

Commit e48d25d

Browse files
committed
copy prompt
1 parent d16ddef commit e48d25d

File tree

8 files changed

+171
-21
lines changed

8 files changed

+171
-21
lines changed

packages/next/src/next-devtools/dev-overlay/components/copy-button/index.tsx

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,35 @@ function useCopyModern(content: string) {
133133
const useCopy =
134134
typeof React.useActionState === 'function' ? useCopyModern : useCopyLegacy
135135

136-
export function CopyButton({
137-
actionLabel,
138-
successLabel,
139-
content,
140-
icon,
141-
disabled,
142-
...props
143-
}: React.HTMLProps<HTMLButtonElement> & {
136+
type CopyButtonProps = React.HTMLProps<HTMLButtonElement> & {
144137
actionLabel: string
145138
successLabel: string
146-
content: string
147139
icon?: React.ReactNode
148-
}) {
149-
const [copyState, copy, reset, isPending] = useCopy(content)
140+
}
141+
142+
export function CopyButton(
143+
props: CopyButtonProps & { content?: string; getContent?: () => string }
144+
) {
145+
const {
146+
content,
147+
getContent,
148+
actionLabel,
149+
successLabel,
150+
icon,
151+
disabled,
152+
...rest
153+
} = props
154+
const getContentString = (): string => {
155+
if (content) {
156+
return content
157+
}
158+
if (getContent) {
159+
return getContent()
160+
}
161+
return ''
162+
}
163+
const contentString = getContentString()
164+
const [copyState, copy, reset, isPending] = useCopy(contentString)
150165

151166
const error = copyState.state === 'error' ? copyState.error : null
152167
React.useEffect(() => {
@@ -186,7 +201,7 @@ export function CopyButton({
186201

187202
return (
188203
<button
189-
{...props}
204+
{...rest}
190205
type="button"
191206
title={label}
192207
aria-label={label}

packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const renderTestComponent = () => {
2929
installed: '15.0.0',
3030
staleness: 'fresh',
3131
}}
32+
generateAIPrompt={() => ''}
3233
>
3334
Module not found: Cannot find module './missing-module'
3435
</ErrorOverlayLayout>

packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface ErrorOverlayLayoutProps extends ErrorBaseProps {
5252
activeIdx?: number
5353
setActiveIndex?: (index: number) => void
5454
dialogResizerRef?: React.RefObject<HTMLDivElement | null>
55+
generateAIPrompt: () => string
5556
}
5657

5758
export function ErrorOverlayLayout({
@@ -70,6 +71,7 @@ export function ErrorOverlayLayout({
7071
setActiveIndex,
7172
isTurbopack,
7273
dialogResizerRef,
74+
generateAIPrompt,
7375
// This prop is used to animate the dialog, it comes from a parent component (<ErrorOverlay>)
7476
// If it's not being passed, we should just render the component as it is being
7577
// used without the context of a parent component that controls its state (e.g. Storybook).
@@ -151,7 +153,11 @@ export function ErrorOverlayLayout({
151153
/>
152154
)}
153155
</span>
154-
<ErrorOverlayToolbar error={error} debugInfo={debugInfo} />
156+
<ErrorOverlayToolbar
157+
error={error}
158+
debugInfo={debugInfo}
159+
generateAIPrompt={generateAIPrompt}
160+
/>
155161
</div>
156162
<ErrorMessage errorMessage={errorMessage} />
157163
</ErrorOverlayDialogHeader>
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { CopyButton } from '../../copy-button'
22

3-
export function CopyStackTraceButton({ error }: { error: Error }) {
3+
export function CopyStackTraceButton({
4+
error,
5+
generateAIPrompt,
6+
}: {
7+
error: Error
8+
generateAIPrompt: () => string
9+
}) {
410
return (
511
<CopyButton
612
data-nextjs-data-runtime-error-copy-stack
713
className="copy-stack-trace-button"
8-
actionLabel="Copy Stack Trace"
9-
successLabel="Stack Trace Copied"
10-
content={error.stack || ''}
11-
disabled={!error.stack}
14+
actionLabel="Copy AI Debug Prompt"
15+
successLabel="AI Debug Prompt Copied"
16+
getContent={generateAIPrompt}
17+
disabled={!error}
1218
/>
1319
)
1420
}

packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-toolbar/error-overlay-toolbar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ type ErrorOverlayToolbarProps = {
77
error: Error
88
debugInfo: DebugInfo | undefined
99
feedbackButton?: React.ReactNode
10+
generateAIPrompt: () => string
1011
}
1112

1213
export function ErrorOverlayToolbar({
1314
error,
1415
debugInfo,
1516
feedbackButton,
17+
generateAIPrompt,
1618
}: ErrorOverlayToolbarProps) {
1719
return (
1820
<span className="error-overlay-toolbar">
1921
{/* TODO: Move the button inside and remove the feedback on the footer of the error overlay. */}
2022
{feedbackButton}
21-
<CopyStackTraceButton error={error} />
23+
<CopyStackTraceButton error={error} generateAIPrompt={generateAIPrompt} />
2224
<DocsLinkButton errorMessage={error.message} />
2325
<NodejsInspectorButton
2426
devtoolsFrontendUrl={debugInfo?.devtoolsFrontendUrl}

packages/next/src/next-devtools/dev-overlay/container/build-error.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,42 @@ export const BuildError: React.FC<BuildErrorProps> = function BuildError({
3737
[message]
3838
)
3939

40+
const generateAIPrompt = useCallback(() => {
41+
const parts: string[] = []
42+
43+
// 1. Error Type
44+
parts.push(`## Error Type\nBuild Error`)
45+
46+
// 2. Error Message
47+
if (formattedMessage) {
48+
parts.push(`## Error Message\n${formattedMessage}`)
49+
}
50+
51+
// 3. Build Output (decoded stderr)
52+
if (message) {
53+
const decodedOutput = stripAnsi(message)
54+
parts.push(`## Build Output\n${decodedOutput}`)
55+
}
56+
57+
// Format as AI prompt
58+
const prompt = `Fix this error in Next.js app:
59+
60+
${parts.join('\n\n')}
61+
62+
Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})
63+
64+
Explain what's wrong and fix it.`
65+
66+
return prompt
67+
}, [message, formattedMessage, props.versionInfo])
68+
4069
return (
4170
<ErrorOverlayLayout
4271
errorType="Build Error"
4372
errorMessage={formattedMessage}
4473
onClose={noop}
4574
error={error}
75+
generateAIPrompt={generateAIPrompt}
4676
{...props}
4777
>
4878
<Terminal content={message} />

packages/next/src/next-devtools/dev-overlay/container/errors.tsx

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useRef, Suspense } from 'react'
1+
import { useMemo, useRef, Suspense, useCallback } from 'react'
22
import type { DebugInfo } from '../../shared/types'
33
import { Overlay, OverlayBackdrop } from '../components/overlay'
44
import { RuntimeError } from './runtime-error'
@@ -15,9 +15,12 @@ import {
1515
NEXTJS_HYDRATION_ERROR_LINK,
1616
} from '../../shared/react-19-hydration-error'
1717
import type { ReadyRuntimeError } from '../utils/get-error-by-type'
18+
import { useFrames } from '../utils/get-error-by-type'
1819
import type { ErrorBaseProps } from '../components/errors/error-overlay/error-overlay'
1920
import type { HydrationErrorState } from '../../shared/hydration-error'
2021
import { useActiveRuntimeError } from '../hooks/use-active-runtime-error'
22+
import { formatCodeFrame } from '../components/code-frame/parse-code-frame'
23+
import stripAnsi from 'next/dist/compiled/strip-ansi'
2124

2225
export interface ErrorsProps extends ErrorBaseProps {
2326
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
@@ -132,6 +135,88 @@ export function Errors({
132135
setActiveIndex,
133136
} = useActiveRuntimeError({ runtimeErrors, getSquashedHydrationErrorDetails })
134137

138+
// Get parsed frames data
139+
const frames = useFrames(activeError)
140+
141+
const firstFrame = useMemo(() => {
142+
const firstFirstPartyFrameIndex = frames.findIndex(
143+
(entry) =>
144+
!entry.ignored &&
145+
Boolean(entry.originalCodeFrame) &&
146+
Boolean(entry.originalStackFrame)
147+
)
148+
149+
return frames[firstFirstPartyFrameIndex] ?? null
150+
}, [frames])
151+
152+
const generateAIPrompt = useCallback(() => {
153+
if (!activeError) return ''
154+
155+
const parts: string[] = []
156+
157+
// 1. Error Type
158+
if (errorType) {
159+
parts.push(`## Error Type\n${errorType}`)
160+
}
161+
162+
// 2. Error Message
163+
const error = activeError.error
164+
let message = error.message
165+
if ('environmentName' in error && error.environmentName) {
166+
const envPrefix = `[ ${error.environmentName} ] `
167+
if (message.startsWith(envPrefix)) {
168+
message = message.slice(envPrefix.length)
169+
}
170+
}
171+
if (message) {
172+
parts.push(`## Error Message\n${message}`)
173+
}
174+
175+
// 3. Code Frame (decoded)
176+
if (firstFrame?.originalCodeFrame) {
177+
const decodedCodeFrame = stripAnsi(
178+
formatCodeFrame(firstFrame.originalCodeFrame)
179+
)
180+
parts.push(`## Code Frame\n${decodedCodeFrame}`)
181+
}
182+
183+
// 4. Call Stack (using parsed frames)
184+
if (frames.length > 0) {
185+
const visibleFrames = frames.filter((frame) => !frame.ignored)
186+
if (visibleFrames.length > 0) {
187+
const stackLines = visibleFrames
188+
.map((frame) => {
189+
if (frame.originalStackFrame) {
190+
const { methodName, file, lineNumber, column } =
191+
frame.originalStackFrame
192+
return ` at ${methodName} (${file}:${lineNumber}:${column})`
193+
} else if (frame.sourceStackFrame) {
194+
const { methodName, file, lineNumber, column } =
195+
frame.sourceStackFrame
196+
return ` at ${methodName} (${file}:${lineNumber}:${column})`
197+
}
198+
return ''
199+
})
200+
.filter(Boolean)
201+
202+
if (stackLines.length > 0) {
203+
parts.push(`## Call Stack\n${stackLines.join('\n')}`)
204+
}
205+
}
206+
}
207+
208+
// Format as AI prompt
209+
const prompt = `Fix this error in Next.js app:
210+
211+
${parts.join('\n\n')}
212+
213+
Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})
214+
215+
Explain what's wrong and fix it.`
216+
217+
return prompt
218+
}, [activeError, errorType, firstFrame, frames, props.versionInfo])
219+
135220
if (isLoading) {
136221
// TODO: better loading state
137222
return (
@@ -168,6 +253,7 @@ export function Errors({
168253
activeIdx={activeIdx}
169254
setActiveIndex={setActiveIndex}
170255
dialogResizerRef={dialogResizerRef}
256+
generateAIPrompt={generateAIPrompt}
171257
{...props}
172258
>
173259
<div className="error-overlay-notes-container">

packages/next/src/next-devtools/dev-overlay/utils/get-error-by-type.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ export type ReadyRuntimeError = {
1414
type: 'runtime' | 'console' | 'recoverable'
1515
}
1616

17-
export const useFrames = (error: ReadyRuntimeError): OriginalStackFrame[] => {
17+
export const useFrames = (
18+
error: ReadyRuntimeError | null
19+
): OriginalStackFrame[] => {
20+
if (!error) return []
21+
1822
if ('use' in React) {
1923
const frames = error.frames
2024

0 commit comments

Comments
 (0)