import { Box, Text, useInput } from 'ink' import * as React from 'react' import { useState, useCallback, useEffect } from 'react' import { getTheme } from '../utils/theme' import { getMessagesGetter } from '../messages' import type { Message } from '../query' import TextInput from './TextInput' import { logError, getInMemoryErrors } from '../utils/log' import { env } from '../utils/env' import { getGitState, getIsGit, GitRepoState } from '../utils/git' import { useTerminalSize } from '../hooks/useTerminalSize' import { getAnthropicApiKey, getGlobalConfig } from '../utils/config' import { USER_AGENT } from '../utils/http' import { logEvent } from '../services/statsig' import { PRODUCT_NAME } from '../constants/product' import { API_ERROR_MESSAGE_PREFIX, queryQuick } from '../services/claude' import { openBrowser } from '../utils/browser' import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD' import { MACRO } from '../constants/macros' import { GITHUB_ISSUES_REPO_URL } from '../constants/product' type Props = { onDone(result: string): void } type Step = 'userInput' | 'consent' | 'submitting' | 'done' type FeedbackData = { // Removing because of privacy concerns. Add this back in when we have a more // robust tool for viewing feedback data that can de-identify users // user_id: string // session_id: string message_count: number datetime: string description: string platform: string gitRepo: boolean version: string | null transcript: Message[] } export function Bug({ onDone }: Props): React.ReactNode { const [step, setStep] = useState('userInput') const [cursorOffset, setCursorOffset] = useState(0) const [description, setDescription] = useState('') const [feedbackId, setFeedbackId] = useState(null) const [error, setError] = useState(null) const [envInfo, setEnvInfo] = useState<{ isGit: boolean gitState: GitRepoState | null }>({ isGit: false, gitState: null }) const [title, setTitle] = useState(null) const textInputColumns = useTerminalSize().columns - 4 const messages = getMessagesGetter()() useEffect(() => { async function loadEnvInfo() { const isGit = await getIsGit() let gitState: GitRepoState | null = null if (isGit) { gitState = await getGitState() } setEnvInfo({ isGit, gitState }) } void loadEnvInfo() }, []) const exitState = useExitOnCtrlCD(() => process.exit(0)) const submitReport = useCallback(async () => { setStep('done') // setStep('submitting') // setError(null) // setFeedbackId(null) // const reportData = { // message_count: messages.length, // datetime: new Date().toISOString(), // description, // platform: env.platform, // gitRepo: envInfo.isGit, // terminal: env.terminal, // version: MACRO.VERSION, // transcript: messages, // errors: getInMemoryErrors(), // } // const [result, t] = await Promise.all([ // submitFeedback(reportData), // generateTitle(description), // ]) // setTitle(t) // if (result.success) { // if (result.feedbackId) { // setFeedbackId(result.feedbackId) // logEvent('tengu_bug_report_submitted', { // feedback_id: result.feedbackId, // }) // } // setStep('done') // } else { // console.log(result) // setError('Could not submit feedback. Please try again later.') // setStep('userInput') // } }, [description, envInfo.isGit, messages]) useInput((input, key) => { // Allow any key press to close the dialog when done or when there's an error // if (step === 'done') { // if (key.return && feedbackId && title) { // // Open GitHub issue URL when Enter is pressed // const issueUrl = createGitHubIssueUrl(feedbackId, title, description) // void openBrowser(issueUrl) // } // onDone('Bug report submitted') // return // } if (error) { onDone('Error submitting bug report') return } if (key.escape) { onDone('Bug report cancelled') return } if (step === 'consent' && (key.return || input === ' ')) { const issueUrl = createGitHubIssueUrl( feedbackId, description.slice(0, 80), description, ) void openBrowser(issueUrl) onDone('Bug report submitted') } }) const theme = getTheme() return ( <> Submit Bug Report {step === 'userInput' && ( Describe the issue below and copy/paste any errors you see: setStep('consent')} onExitMessage={() => onDone('Bug report cancelled') } cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> {error && ( {error} Press any key to close )} )} {step === 'consent' && ( This report will include: - Your bug description: {description} - Environment info:{' '} {env.platform}, {env.terminal}, v{MACRO.VERSION} {/* {envInfo.gitState && ( - Git repo metadata:{' '} {envInfo.gitState.branchName} {envInfo.gitState.commitHash ? `, ${envInfo.gitState.commitHash.slice(0, 7)}` : ''} {envInfo.gitState.remoteUrl ? ` @ ${envInfo.gitState.remoteUrl}` : ''} {!envInfo.gitState.isHeadOnRemote && ', not synced'} {!envInfo.gitState.isClean && ', has local changes'} )} */} - Model settings (no api keys) {/* We will use your feedback to debug related issues or to improve{' '} {PRODUCT_NAME}'s functionality (eg. to reduce the risk of bugs occurring in the future). Anthropic will not train generative models using feedback from {PRODUCT_NAME}. Press Enter to confirm and submit. */} )} {step === 'submitting' && ( Submitting report… )} {step === 'done' && ( Thank you for your report! {feedbackId && Feedback ID: {feedbackId}} Press Enter to also create a GitHub issue, or any other key to close. )} {exitState.pending ? ( <>Press {exitState.keyName} again to exit ) : step === 'userInput' ? ( <>Enter to continue · Esc to cancel ) : step === 'consent' ? ( <>Enter to open browser to create GitHub issue · Esc to cancel ) : null} ) } function createGitHubIssueUrl( feedbackId: string, title: string, description: string, ): string { const globalConfig = getGlobalConfig() // Get ModelProfile information instead of legacy model info const modelProfiles = globalConfig.modelProfiles || [] const activeProfiles = modelProfiles.filter(p => p.isActive) let modelInfo = '## Models\n' if (activeProfiles.length === 0) { modelInfo += '- No model profiles configured\n' } else { activeProfiles.forEach(profile => { modelInfo += `- ${profile.name}\n` modelInfo += ` - provider: ${profile.provider}\n` modelInfo += ` - model: ${profile.modelName}\n` modelInfo += ` - baseURL: ${profile.baseURL}\n` modelInfo += ` - maxTokens: ${profile.maxTokens}\n` modelInfo += ` - contextLength: ${profile.contextLength}\n` if (profile.reasoningEffort) { modelInfo += ` - reasoning effort: ${profile.reasoningEffort}\n` } }) } const body = encodeURIComponent(` ## Bug Description ${description} ## Environment Info - Platform: ${env.platform} - Terminal: ${env.terminal} - Version: ${MACRO.VERSION || 'unknown'} ${modelInfo}`) return `${GITHUB_ISSUES_REPO_URL}/new?title=${encodeURIComponent(title)}&body=${body}&labels=user-reported,bug` } async function generateTitle(description: string): Promise { const response = await queryQuick({ systemPrompt: [ 'Generate a concise issue title (max 80 chars) that captures the key point of this feedback. Do not include quotes or prefixes like "Feedback:" or "Issue:". If you cannot generate a title, just use "User Feedback".', ], userPrompt: description, }) const title = response.message.content[0]?.type === 'text' ? response.message.content[0].text : 'Bug Report' if (title.startsWith(API_ERROR_MESSAGE_PREFIX)) { return `Bug Report: ${description.slice(0, 60)}${description.length > 60 ? '...' : ''}` } return title } async function submitFeedback( data: FeedbackData, ): Promise<{ success: boolean; feedbackId?: string }> { return { success: true, feedbackId: '123' } // try { // const apiKey = getAnthropicApiKey() // if (!apiKey) { // return { success: false } // } // const response = await fetch( // 'https://api.anthropic.com/api/claude_cli_feedback', // { // method: 'POST', // headers: { // 'Content-Type': 'application/json', // 'User-Agent': USER_AGENT, // 'x-api-key': apiKey, // }, // body: JSON.stringify({ // content: JSON.stringify(data), // }), // }, // ) // if (response.ok) { // const result = await response.json() // if (result?.feedback_id) { // return { success: true, feedbackId: result.feedback_id } // } // logError('Failed to submit feedback: request did not return feedback_id') // return { success: false } // } // logError('Failed to submit feedback:' + response.status) // return { success: false } // } catch (err) { // logError( // 'Error submitting feedback: ' + // (err instanceof Error ? err.message : 'Unknown error'), // ) // return { success: false } // } }