(feedbackId: string, title: string, description: string, errors: Array<{
error?: string;
timestamp?: string;
}>)
| 391 | </Dialog>; |
| 392 | } |
| 393 | export function createGitHubIssueUrl(feedbackId: string, title: string, description: string, errors: Array<{ |
| 394 | error?: string; |
| 395 | timestamp?: string; |
| 396 | }>): string { |
| 397 | const sanitizedTitle = redactSensitiveInfo(title); |
| 398 | const sanitizedDescription = redactSensitiveInfo(description); |
| 399 | const bodyPrefix = `**Bug Description**\n${sanitizedDescription}\n\n` + `**Environment Info**\n` + `- Platform: ${env.platform}\n` + `- Terminal: ${env.terminal}\n` + `- Version: ${MACRO.VERSION || 'unknown'}\n` + `- Feedback ID: ${feedbackId}\n` + `\n**Errors**\n\`\`\`json\n`; |
| 400 | const errorSuffix = `\n\`\`\`\n`; |
| 401 | const errorsJson = jsonStringify(errors); |
| 402 | const baseUrl = `${GITHUB_ISSUES_REPO_URL}/new?title=${encodeURIComponent(sanitizedTitle)}&labels=user-reported,bug&body=`; |
| 403 | const truncationNote = `\n**Note:** Content was truncated.\n`; |
| 404 | const encodedPrefix = encodeURIComponent(bodyPrefix); |
| 405 | const encodedSuffix = encodeURIComponent(errorSuffix); |
| 406 | const encodedNote = encodeURIComponent(truncationNote); |
| 407 | const encodedErrors = encodeURIComponent(errorsJson); |
| 408 | |
| 409 | // Calculate space available for errors |
| 410 | const spaceForErrors = GITHUB_URL_LIMIT - baseUrl.length - encodedPrefix.length - encodedSuffix.length - encodedNote.length; |
| 411 | |
| 412 | // If description alone exceeds limit, truncate everything |
| 413 | if (spaceForErrors <= 0) { |
| 414 | const ellipsis = encodeURIComponent('…'); |
| 415 | const buffer = 50; // Extra safety margin |
| 416 | const maxEncodedLength = GITHUB_URL_LIMIT - baseUrl.length - ellipsis.length - encodedNote.length - buffer; |
| 417 | const fullBody = bodyPrefix + errorsJson + errorSuffix; |
| 418 | let encodedFullBody = encodeURIComponent(fullBody); |
| 419 | if (encodedFullBody.length > maxEncodedLength) { |
| 420 | encodedFullBody = encodedFullBody.slice(0, maxEncodedLength); |
| 421 | // Don't cut in middle of %XX sequence |
| 422 | const lastPercent = encodedFullBody.lastIndexOf('%'); |
| 423 | if (lastPercent >= encodedFullBody.length - 2) { |
| 424 | encodedFullBody = encodedFullBody.slice(0, lastPercent); |
| 425 | } |
| 426 | } |
| 427 | return baseUrl + encodedFullBody + ellipsis + encodedNote; |
| 428 | } |
| 429 | |
| 430 | // If errors fit, no truncation needed |
| 431 | if (encodedErrors.length <= spaceForErrors) { |
| 432 | return baseUrl + encodedPrefix + encodedErrors + encodedSuffix; |
| 433 | } |
| 434 | |
| 435 | // Truncate errors to fit (prioritize keeping description) |
| 436 | // Slice encoded errors directly, then trim to avoid cutting %XX sequences |
| 437 | const ellipsis = encodeURIComponent('…'); |
| 438 | const buffer = 50; // Extra safety margin |
| 439 | let truncatedEncodedErrors = encodedErrors.slice(0, spaceForErrors - ellipsis.length - buffer); |
| 440 | // If we cut in middle of %XX, back up to before the % |
| 441 | const lastPercent = truncatedEncodedErrors.lastIndexOf('%'); |
| 442 | if (lastPercent >= truncatedEncodedErrors.length - 2) { |
| 443 | truncatedEncodedErrors = truncatedEncodedErrors.slice(0, lastPercent); |
| 444 | } |
| 445 | return baseUrl + encodedPrefix + truncatedEncodedErrors + ellipsis + encodedSuffix + encodedNote; |
| 446 | } |
| 447 | async function generateTitle(description: string, abortSignal: AbortSignal): Promise<string> { |
| 448 | try { |
| 449 | const response = await queryHaiku({ |
no test coverage detected