diff --git a/src/main/github/client-merge-queue-auto-merge.test.ts b/src/main/github/client-merge-queue-auto-merge.test.ts index b1af6ee3f7d..89dc008ac79 100644 --- a/src/main/github/client-merge-queue-auto-merge.test.ts +++ b/src/main/github/client-merge-queue-auto-merge.test.ts @@ -258,7 +258,7 @@ describe('GitHub GraphQL rate-limit guard', () => { repo: 'orca', host: 'github.com' }) - ).resolves.toEqual({ + ).resolves.toMatchObject({ ok: false, error: 'GitHub does not support auto-merge for stacked pull requests.' }) @@ -279,7 +279,7 @@ describe('GitHub GraphQL rate-limit guard', () => { repo: 'orca', host: 'github.com' }) - ).resolves.toEqual({ + ).resolves.toMatchObject({ ok: false, error: 'This pull request can already be merged. Use Merge instead of auto-merge.' }) @@ -301,7 +301,7 @@ describe('GitHub GraphQL rate-limit guard', () => { repo: 'orca', host: 'github.com' }) - ).resolves.toEqual({ + ).resolves.toMatchObject({ ok: false, error: `Failed to enable auto-merge for pull request: ${providerError}` }) @@ -318,9 +318,11 @@ describe('GitHub GraphQL rate-limit guard', () => { repo: 'orca', host: 'github.com' }) - ).resolves.toEqual({ + ).resolves.toMatchObject({ ok: false, - error: 'Failed to disable auto-merge for pull request: gh: pull request is already closed' + error: 'Failed to disable auto-merge for pull request: gh: pull request is already closed', + errorCode: 'github.autoMergeDisableFailed', + errorDetail: 'gh: pull request is already closed' }) }) diff --git a/src/main/github/client-pr-checks.test.ts b/src/main/github/client-pr-checks.test.ts index 1b78ce5380f..c3557440ce7 100644 --- a/src/main/github/client-pr-checks.test.ts +++ b/src/main/github/client-pr-checks.test.ts @@ -614,7 +614,7 @@ describe('getPRChecks', () => { const result = await rerunPRChecks('/repo-root', 42, { failedOnly: true }) - expect(result).toEqual({ + expect(result).toMatchObject({ ok: false, error: 'GitHub resource to rerun was not found — it may have expired or been deleted.' }) @@ -653,7 +653,7 @@ describe('getPRChecks', () => { ['api', '-X', 'POST', 'repos/acme/widgets/check-runs/88/rerequest'], expect.objectContaining({ cwd: '/repo-root' }) ) - expect(result).toEqual({ + expect(result).toMatchObject({ ok: false, error: 'GitHub resource to rerun was not found — it may have expired or been deleted.' }) diff --git a/src/main/github/client/check/rerun-pr-checks.ts b/src/main/github/client/check/rerun-pr-checks.ts index f2bcea83a64..39298f3f6f6 100644 --- a/src/main/github/client/check/rerun-pr-checks.ts +++ b/src/main/github/client/check/rerun-pr-checks.ts @@ -6,6 +6,7 @@ import { classifyRerunChecksError } from '../../gh-error-classification' import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository' import { getPRChecks } from './get-pr-checks' import { parseActionsRunId } from './check-detail-field-mapping' +import { codeHostFailure, classifiedFailure } from '../../../../shared/code-host-failure-copy' export async function rerunPRChecks( repoPath: string, prNumber: number, @@ -24,7 +25,7 @@ export async function rerunPRChecks( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } const checks = await getPRChecks( @@ -54,12 +55,9 @@ export async function rerunPRChecks( ) if (workflowRunIds.size === 0 && checkRunIds.size === 0) { - return { - ok: false, - error: options.failedOnly - ? 'No failed GitHub Actions checks to rerun.' - : 'No rerunnable checks found.' - } + return codeHostFailure( + options.failedOnly ? 'github.noFailedChecks' : 'github.noRerunnableChecks' + ) } let count = 0 @@ -90,15 +88,14 @@ export async function rerunPRChecks( return { ok: true, count } } catch (err) { const { stderr } = extractExecError(err) - const classified = classifyRerunChecksError(stderr).message + const classified = classifyRerunChecksError(stderr) // Rerun POSTs are not idempotent; report partial success before the user retries. - return { - ok: false, - error: - count > 0 - ? `${classified} (${count} rerun${count === 1 ? '' : 's'} already started)` - : classified - } + return count > 0 + ? { + ok: false, + error: `${classified.message} (${count} rerun${count === 1 ? '' : 's'} already started)` + } + : classifiedFailure(classified) } finally { release() } diff --git a/src/main/github/client/create/add-pr-review-comment.ts b/src/main/github/client/create/add-pr-review-comment.ts index 6096bd05b26..6b2f4016c66 100644 --- a/src/main/github/client/create/add-pr-review-comment.ts +++ b/src/main/github/client/create/add-pr-review-comment.ts @@ -11,6 +11,7 @@ import { } from '../../gh-utils' import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository' import { mapReviewCommentResponse } from './../map/review-comment-response' +import { codeHostFailure, classifiedFailure } from '../../../../shared/code-host-failure-copy' export async function addPRReviewComment( args: GitHubPRReviewCommentInput & { connectionId?: string | null @@ -24,7 +25,7 @@ export async function addPRReviewComment( args.localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() try { @@ -56,7 +57,7 @@ export async function addPRReviewComment( const data = JSON.parse(stdout) as Parameters[0] // Reject synthetic fallback IDs that replies and reactions could otherwise target. if (typeof data.id !== 'number' || !Number.isSafeInteger(data.id) || data.id < 1) { - return { ok: false, error: 'Unexpected response from GitHub' } + return codeHostFailure('github.unexpectedResponse') } return { ok: true, @@ -64,7 +65,7 @@ export async function addPRReviewComment( } } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGhError(stderr).message } + return classifiedFailure(classifyGhError(stderr)) } finally { release() } @@ -89,7 +90,7 @@ export async function addPRReviewCommentReply( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() try { @@ -106,7 +107,7 @@ export async function addPRReviewCommentReply( ) const data = JSON.parse(stdout) as Parameters[0] if (typeof data.id !== 'number' || !Number.isSafeInteger(data.id) || data.id < 1) { - return { ok: false, error: 'Unexpected response from GitHub' } + return codeHostFailure('github.unexpectedResponse') } return { ok: true, @@ -114,7 +115,7 @@ export async function addPRReviewCommentReply( } } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGhError(stderr).message } + return classifiedFailure(classifyGhError(stderr)) } finally { release() } diff --git a/src/main/github/client/merge/merge-pr.ts b/src/main/github/client/merge/merge-pr.ts index d534b85b109..095fdd56aee 100644 --- a/src/main/github/client/merge/merge-pr.ts +++ b/src/main/github/client/merge/merge-pr.ts @@ -9,6 +9,7 @@ import { detectRepositoryMergeMetadata } from './../detect/repository-merge-meta import type { PullRequestLookupData } from './../lookup/pull-request-lookup-data' import { getRestPRByNumber, getPRByNumber } from './../lookup/pr-number-lookup' import { STACK_METADATA_UNAVAILABLE_ERROR } from './../lookup/pr-stack-summary-cache' +import { codeHostFailure } from '../../../../shared/code-host-failure-copy' export async function mergePR( repoPath: string, prNumber: number, @@ -24,7 +25,7 @@ export async function mergePR( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() let concurrencySlotHeld = true diff --git a/src/main/github/client/merge/pr-auto-merge.ts b/src/main/github/client/merge/pr-auto-merge.ts index 3ee309e7cdf..a21c513d7d2 100644 --- a/src/main/github/client/merge/pr-auto-merge.ts +++ b/src/main/github/client/merge/pr-auto-merge.ts @@ -7,6 +7,11 @@ import { githubPRStackExecutionScope, type GhExecOptions } from './../github-exe import { detectRepositoryMergeMetadata } from './../detect/repository-merge-metadata' import { getRestPRByNumber } from './../lookup/pr-number-lookup' import { errorMessage } from '../../../../shared/error-message' +import { + classifiedFailure, + codeHostFailure, + type CodeHostFailure +} from '../../../../shared/code-host-failure-copy' export const PR_AUTO_MERGE_IDENTITY_JSON_FIELDS = 'id,headRefOid,baseRefName' export const GITHUB_AUTO_MERGE_METHODS: Record = @@ -17,14 +22,16 @@ export const GITHUB_AUTO_MERGE_METHODS: Record { const logins = reviewers.map((reviewer) => reviewer.trim()).filter(Boolean) if (logins.length === 0) { - return { ok: false, error: 'Enter at least one reviewer' } + return codeHostFailure('github.reviewerRequired') } const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution( repoPath, @@ -47,7 +48,7 @@ export async function requestPRReviewers( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() try { @@ -77,7 +78,7 @@ export async function removePRReviewers( ): Promise<{ ok: true } | { ok: false; error: string }> { const logins = reviewers.map((reviewer) => reviewer.trim()).filter(Boolean) if (logins.length === 0) { - return { ok: false, error: 'Enter at least one reviewer' } + return codeHostFailure('github.reviewerRequired') } const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution( repoPath, @@ -86,7 +87,7 @@ export async function removePRReviewers( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() try { diff --git a/src/main/github/client/update/pr-state.ts b/src/main/github/client/update/pr-state.ts index 46faf8eecb2..6b2e742daec 100644 --- a/src/main/github/client/update/pr-state.ts +++ b/src/main/github/client/update/pr-state.ts @@ -8,6 +8,7 @@ import { type LocalGitExecOptions } from '../../gh-utils' import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository' +import { codeHostFailure, classifiedFailure } from '../../../../shared/code-host-failure-copy' export async function updatePRState( repoPath: string, prNumber: number, @@ -23,7 +24,7 @@ export async function updatePRState( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() @@ -41,7 +42,7 @@ export async function updatePRState( } catch (err) { const message = err instanceof Error ? err.message : typeof err === 'string' ? err : 'Unknown error' - return { ok: false, error: classifyPullRequestUpdateError(message).message } + return classifiedFailure(classifyPullRequestUpdateError(message)) } finally { release() } diff --git a/src/main/github/gh-error-classification.ts b/src/main/github/gh-error-classification.ts index cffdef92542..0167bec3fd4 100644 --- a/src/main/github/gh-error-classification.ts +++ b/src/main/github/gh-error-classification.ts @@ -1,28 +1,24 @@ import type { ClassifiedError } from '../../shared/classified-error' +import { classifiedCodeHostError as classified } from '../../shared/code-host-failure-copy' export function classifyGhError(stderr: string): ClassifiedError { const s = stderr.toLowerCase() + const trimmed = stderr.trim() // Rate limits also carry HTTP 403; classify them before permission failures. if (s.includes('rate limit')) { - return { - type: 'rate_limited', - message: 'GitHub rate limit hit. Try again in a few minutes.' - } + return classified('rate_limited', 'github.rateLimited') } if (s.includes('http 403') || s.includes('resource not accessible')) { - return { - type: 'permission_denied', - message: "You don't have permission to edit this issue. Check your GitHub token scopes." - } + return classified('permission_denied', 'github.issuePermissionDenied') } if (s.includes('http 404') || s.includes('could not resolve to a repository')) { - return { type: 'not_found', message: 'Issue not found — it may have been deleted.' } + return classified('not_found', 'issueNotFound') } if (s.includes('has disabled issues')) { - return { type: 'issues_disabled', message: 'Issues are disabled on this repository.' } + return classified('issues_disabled', 'github.issuesDisabled') } if (s.includes('http 422') || s.includes('validation failed')) { - return { type: 'validation_error', message: `Invalid update — ${stderr.trim()}` } + return classified('validation_error', 'invalidUpdate', trimmed) } if ( s.includes('timeout') || @@ -30,9 +26,9 @@ export function classifyGhError(stderr: string): ClassifiedError { s.includes('network') || s.includes('could not resolve host') ) { - return { type: 'network_error', message: 'Network error — check your connection.' } + return classified('network_error', 'networkError') } - return { type: 'unknown', message: `Failed to update issue: ${stderr.trim()}` } + return classified('unknown', 'issueUpdateFailed', trimmed) } // Shared classification uses mutation copy; list banners need read-specific wording. @@ -52,24 +48,33 @@ export function classifyListIssuesError(stderr: string): ClassifiedError { return { type: c.type, message: readMessages[c.type] } } -// Preserve classification while using PR-specific wording. +export type PullRequestUpdateAction = + | 'github.pullRequestUpdateFailed' + | 'github.autoMergeEnableFailed' + | 'github.autoMergeDisableFailed' + | 'github.mergeQueueAddFailed' + +// Preserve classification while using PR-specific wording; `action` names the operation for unknown failures. export function classifyPullRequestUpdateError( stderr: string, - action = 'update pull request' + action: PullRequestUpdateAction = 'github.pullRequestUpdateFailed' ): ClassifiedError { const c = classifyGhError(stderr) const trimmed = stderr.trim() - const pullRequestMessages: Record = { - permission_denied: - "You don't have permission to edit this pull request. Check your GitHub token scopes.", - not_found: 'Pull request not found — it may have been deleted.', - issues_disabled: c.message, - validation_error: `Invalid update — ${trimmed}`, - rate_limited: c.message, - network_error: c.message, - unknown: `Failed to ${action}: ${trimmed}` + switch (c.type) { + case 'permission_denied': + return classified(c.type, 'github.pullRequestPermissionDenied') + case 'not_found': + return classified(c.type, 'github.pullRequestNotFound') + case 'validation_error': + return classified(c.type, 'invalidUpdate', trimmed) + case 'unknown': + return classified(c.type, action, trimmed) + case 'issues_disabled': + case 'rate_limited': + case 'network_error': + return c } - return { type: c.type, message: pullRequestMessages[c.type] } } // Pagination needs typed failures so an outage cannot masquerade as end-of-data. @@ -82,15 +87,18 @@ export function classifyListPrsError(stderr: string): ClassifiedError { export function classifyRerunChecksError(stderr: string): ClassifiedError { const c = classifyGhError(stderr) const trimmed = stderr.trim() - const rerunMessages: Record = { - permission_denied: - "You don't have permission to rerun checks on this repository. Check your GitHub token scopes.", - not_found: 'GitHub resource to rerun was not found — it may have expired or been deleted.', - issues_disabled: `Failed to rerun checks: ${trimmed}`, - validation_error: `Could not rerun checks — ${trimmed}`, - rate_limited: c.message, - network_error: c.message, - unknown: `Failed to rerun checks: ${trimmed}` + switch (c.type) { + case 'permission_denied': + return classified(c.type, 'github.rerunPermissionDenied') + case 'not_found': + return classified(c.type, 'github.rerunTargetNotFound') + case 'validation_error': + return classified(c.type, 'github.rerunRejected', trimmed) + case 'issues_disabled': + case 'unknown': + return classified(c.type, 'github.rerunFailed', trimmed) + case 'rate_limited': + case 'network_error': + return c } - return { type: c.type, message: rerunMessages[c.type] } } diff --git a/src/main/github/gh-utils.test.ts b/src/main/github/gh-utils.test.ts index 9c97ff3fb31..12ce1c55611 100644 --- a/src/main/github/gh-utils.test.ts +++ b/src/main/github/gh-utils.test.ts @@ -784,7 +784,7 @@ describe('gh error classification', () => { it('classifies "has disabled issues" stderr as issues_disabled', () => { const stderr = "Command failed: gh issue list --limit 36 --json number,title,state --repo brennanb2025/orca --state open\nthe 'brennanb2025/orca' repository has disabled issues" - expect(classifyGhError(stderr)).toEqual({ + expect(classifyGhError(stderr)).toMatchObject({ type: 'issues_disabled', message: 'Issues are disabled on this repository.' }) diff --git a/src/main/github/github-pr-stack-async-merge.ts b/src/main/github/github-pr-stack-async-merge.ts index d5375a468ac..299c5e7d6c9 100644 --- a/src/main/github/github-pr-stack-async-merge.ts +++ b/src/main/github/github-pr-stack-async-merge.ts @@ -6,6 +6,7 @@ import { type GitHubApiRepository, type GitHubRepoExecOptions } from './github-api-repository' +import { codeHostFailure } from '../../shared/code-host-failure-copy' const POLL_INTERVAL_MS = 1_000 const MAX_POLLS = 180 @@ -103,7 +104,7 @@ export async function mergeGitHubPRStack(args: { } } if (!submitted) { - return { ok: false, error: 'GitHub returned an invalid stack merge response.' } + return codeHostFailure('github.stackMergeInvalidResponse') } let result = classifyResult(submitted) if (result.kind === 'success') { @@ -131,7 +132,7 @@ export async function mergeGitHubPRStack(args: { } } if (!response) { - return { ok: false, error: 'GitHub returned an invalid stack merge response.' } + return codeHostFailure('github.stackMergeInvalidResponse') } result = classifyResult(response) if (result.kind === 'success') { @@ -141,5 +142,5 @@ export async function mergeGitHubPRStack(args: { return { ok: false, error: result.message } } } - return { ok: false, error: 'GitHub is still merging this stack. Refresh to check its status.' } + return codeHostFailure('github.stackMergePending') } diff --git a/src/main/github/github-pr-stack.test.ts b/src/main/github/github-pr-stack.test.ts index 243fdc057ab..0bc4d901be4 100644 --- a/src/main/github/github-pr-stack.test.ts +++ b/src/main/github/github-pr-stack.test.ts @@ -322,7 +322,7 @@ describe('mergeGitHubPRStack', () => { }) await vi.advanceTimersByTimeAsync(180_000) - await expect(result).resolves.toEqual({ + await expect(result).resolves.toMatchObject({ ok: false, error: 'GitHub is still merging this stack. Refresh to check its status.' }) diff --git a/src/main/github/issue-comment.ts b/src/main/github/issue-comment.ts index f2f7ab50d94..88f5f642c63 100644 --- a/src/main/github/issue-comment.ts +++ b/src/main/github/issue-comment.ts @@ -6,6 +6,7 @@ import { resolveIssueGitHubApiRepositorySource } from './github-api-repository' import { acquire, classifyGhError, ghExecFileAsync, release } from './gh-utils' +import { codeHostFailure, classifiedFailure } from '../../shared/code-host-failure-copy' // An explicit target keeps replies bound to the conversation; otherwise use the selected source. export async function addIssueComment( @@ -33,7 +34,7 @@ export async function addIssueComment( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() try { @@ -57,7 +58,7 @@ export async function addIssueComment( html_url?: string } if (typeof data.id !== 'number' || !Number.isSafeInteger(data.id) || data.id < 1) { - return { ok: false, error: 'Unexpected response from GitHub' } + return codeHostFailure('github.unexpectedResponse') } const comment: PRComment = { id: data.id, @@ -72,7 +73,7 @@ export async function addIssueComment( return { ok: true, comment } } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGhError(stderr).message } + return classifiedFailure(classifyGhError(stderr)) } finally { release() } diff --git a/src/main/github/issue-create.ts b/src/main/github/issue-create.ts index 9c75757997d..4579e04365f 100644 --- a/src/main/github/issue-create.ts +++ b/src/main/github/issue-create.ts @@ -9,6 +9,7 @@ import { resolveIssueGitHubApiRepositorySource } from './github-api-repository' import { acquire, extractExecError, ghExecFileAsync, release } from './gh-utils' +import { codeHostFailure } from '../../shared/code-host-failure-copy' function githubIssueErrorMessage(error: unknown): string { const { stderr, stdout } = extractExecError(error) @@ -27,7 +28,7 @@ export async function createIssue( ): Promise { const trimmedTitle = title.trim() if (!trimmedTitle) { - return { ok: false, error: 'Title is required' } + return codeHostFailure('titleRequired') } const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution( repoPath, @@ -44,7 +45,7 @@ export async function createIssue( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } await acquire() try { @@ -78,7 +79,7 @@ export async function createIssue( const { stdout } = await createIssueRequest('') data = parseIssue(stdout) if (typeof data.number !== 'number') { - return { ok: false, error: 'Unexpected response from GitHub' } + return codeHostFailure('github.unexpectedResponse') } try { @@ -99,7 +100,7 @@ export async function createIssue( } if (typeof data.number !== 'number') { - return { ok: false, error: 'Unexpected response from GitHub' } + return codeHostFailure('github.unexpectedResponse') } return { ok: true, diff --git a/src/main/github/issue-update.ts b/src/main/github/issue-update.ts index 718398c57af..fa72852bde7 100644 --- a/src/main/github/issue-update.ts +++ b/src/main/github/issue-update.ts @@ -7,6 +7,8 @@ import { type GitHubApiRepository } from './github-api-repository' import { acquire, classifyGhError, ghExecFileAsync, release } from './gh-utils' +import { codeHostFailure, combinedClassifiedFailure } from '../../shared/code-host-failure-copy' +import type { ClassifiedError } from '../../shared/classified-error' export async function updateIssue( repoPath: string, @@ -33,11 +35,11 @@ export async function updateIssue( localGitOptions ) if (!ownerRepo) { - return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' } + return codeHostFailure('github.repoUnresolved') } const repo = `${ownerRepo.owner}/${ownerRepo.repo}` - const errors: string[] = [] + const errors: ClassifiedError[] = [] if (updates.state) { await acquire() @@ -58,7 +60,7 @@ export async function updateIssue( } catch (err) { // Why: gh exits 0 when the issue is already in the target state, so any failure is real. const stderr = err instanceof Error ? err.message : String(err) - errors.push(classifyGhError(stderr).message) + errors.push(classifyGhError(stderr)) } finally { release() } @@ -80,7 +82,7 @@ export async function updateIssue( ) } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - errors.push(classifyGhError(stderr).message) + errors.push(classifyGhError(stderr)) } finally { release() } @@ -116,14 +118,14 @@ export async function updateIssue( await ghExecFileAsync(editArgs, ghOptions) } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - errors.push(classifyGhError(stderr).message) + errors.push(classifyGhError(stderr)) } finally { release() } } if (errors.length > 0) { - return { ok: false, error: errors.join('; ') } + return combinedClassifiedFailure(errors) } return { ok: true } } diff --git a/src/main/github/issues.test.ts b/src/main/github/issues.test.ts index f9cb9fca6d1..16314c36ab2 100644 --- a/src/main/github/issues.test.ts +++ b/src/main/github/issues.test.ts @@ -544,7 +544,7 @@ describe('issue source operations', () => { new Error('GraphQL: API rate limit already exceeded for user ID 1.') ) - await expect(updateIssue('/repo-root', 924, { state: 'closed' })).resolves.toEqual({ + await expect(updateIssue('/repo-root', 924, { state: 'closed' })).resolves.toMatchObject({ ok: false, error: 'GitHub rate limit hit. Try again in a few minutes.' }) diff --git a/src/main/gitlab/client-mr-job-ci.test.ts b/src/main/gitlab/client-mr-job-ci.test.ts index bbca5467564..c866c5951be 100644 --- a/src/main/gitlab/client-mr-job-ci.test.ts +++ b/src/main/gitlab/client-mr-job-ci.test.ts @@ -92,7 +92,7 @@ describe('gitlab client — MR operations', () => { it('keeps a missing project and a scope failure as errors, in job-log wording', async () => { glabExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 404: Project Not Found')) - await expect(getJobTrace('/repo', 99, 'upstream', 'conn-1')).resolves.toEqual({ + await expect(getJobTrace('/repo', 99, 'upstream', 'conn-1')).resolves.toMatchObject({ ok: false, error: "Could not find this job's GitLab project." }) @@ -101,7 +101,8 @@ describe('gitlab client — MR operations', () => { const forbidden = await getJobTrace('/repo', 99, 'upstream', 'conn-1') expect(forbidden).toEqual({ ok: false, - error: "You don't have permission to read this job's log. Check your GitLab token scopes." + error: "You don't have permission to read this job's log. Check your GitLab token scopes.", + errorCode: 'gitlab.jobLogPermissionDenied' }) }) diff --git a/src/main/gitlab/client-mr-review-actions.test.ts b/src/main/gitlab/client-mr-review-actions.test.ts index 311306b114c..5a5d314aae6 100644 --- a/src/main/gitlab/client-mr-review-actions.test.ts +++ b/src/main/gitlab/client-mr-review-actions.test.ts @@ -275,7 +275,7 @@ describe('gitlab client — MR operations', () => { await expect( updateMR('/repo', 12, { readyForReview: true }, 'upstream', 'conn-1') - ).resolves.toEqual({ ok: false, error: 'Title is required' }) + ).resolves.toMatchObject({ ok: false, error: 'Title is required' }) expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1) }) @@ -312,7 +312,7 @@ describe('gitlab client — MR operations', () => { }) it('rejects an empty discussion id without calling glab', async () => { - await expect(resolveMRDiscussion('/repo', 12, ' ', true)).resolves.toEqual({ + await expect(resolveMRDiscussion('/repo', 12, ' ', true)).resolves.toMatchObject({ ok: false, error: 'Discussion id is required' }) diff --git a/src/main/gitlab/glab-error-classification.ts b/src/main/gitlab/glab-error-classification.ts index 1d75ad388e1..b42d028a904 100644 --- a/src/main/gitlab/glab-error-classification.ts +++ b/src/main/gitlab/glab-error-classification.ts @@ -1,25 +1,21 @@ import type { ClassifiedError } from '../../shared/classified-error' +import { classifiedCodeHostError as classified } from '../../shared/code-host-failure-copy' import { GlabNonListResponseError } from './glab-api-response' export function classifyGlabError(stderr: string): ClassifiedError { const s = stderr.toLowerCase() + const trimmed = stderr.trim() if (s.includes('http 403') || s.includes('forbidden') || s.includes('insufficient_scope')) { - return { - type: 'permission_denied', - message: "You don't have permission to edit this issue. Check your GitLab token scopes." - } + return classified('permission_denied', 'gitlab.issuePermissionDenied') } if (s.includes('http 404') || s.includes('project not found')) { - return { type: 'not_found', message: 'Issue not found — it may have been deleted.' } + return classified('not_found', 'issueNotFound') } if (s.includes('http 422') || s.includes('unprocessable')) { - return { type: 'validation_error', message: `Invalid update — ${stderr.trim()}` } + return classified('validation_error', 'invalidUpdate', trimmed) } if (s.includes('rate limit') || s.includes('http 429')) { - return { - type: 'rate_limited', - message: 'GitLab rate limit hit. Try again in a few minutes.' - } + return classified('rate_limited', 'gitlab.rateLimited') } if ( s.includes('timeout') || @@ -27,9 +23,9 @@ export function classifyGlabError(stderr: string): ClassifiedError { s.includes('network') || s.includes('could not resolve host') ) { - return { type: 'network_error', message: 'Network error — check your connection.' } + return classified('network_error', 'networkError') } - return { type: 'unknown', message: `Failed to update issue: ${stderr.trim()}` } + return classified('unknown', 'issueUpdateFailed', trimmed) } const LIST_READ_FAILURE = 'Failed to load issues' @@ -63,19 +59,22 @@ export function classifyListFetchError(err: unknown): ClassifiedError { export function classifyJobLogError(stderr: string): ClassifiedError { const c = classifyGlabError(stderr) const trimmed = stderr.trim() - const logMessages: Record = { - permission_denied: - "You don't have permission to read this job's log. Check your GitLab token scopes.", + switch (c.type) { + case 'permission_denied': + return classified(c.type, 'gitlab.jobLogPermissionDenied') // A missing log is already reported as an empty log, so a 404 that reaches here // means the project itself is gone or invisible to this token. - not_found: "Could not find this job's GitLab project.", - issues_disabled: `Failed to load the job log: ${trimmed}`, - validation_error: `Invalid request — ${trimmed}`, - rate_limited: 'GitLab rate limit hit. Try again in a few minutes.', - network_error: 'Network error — check your connection.', - unknown: `Failed to load the job log: ${trimmed}` + case 'not_found': + return classified(c.type, 'gitlab.jobProjectNotFound') + case 'validation_error': + return classified(c.type, 'invalidRequest', trimmed) + case 'issues_disabled': + case 'unknown': + return classified(c.type, 'gitlab.jobLogFailed', trimmed) + case 'rate_limited': + case 'network_error': + return c } - return { type: c.type, message: logMessages[c.type] } } /** Missing/erased traces return empty logs, including ambiguous job 404s; explicit project failures remain errors. */ diff --git a/src/main/gitlab/issue-update.ts b/src/main/gitlab/issue-update.ts index 4de18cd7efa..0b1b36387dd 100644 --- a/src/main/gitlab/issue-update.ts +++ b/src/main/gitlab/issue-update.ts @@ -14,6 +14,8 @@ import { type ProjectRef } from './gl-utils' import { encodedProject } from './project-path-encoding' +import { codeHostFailure, combinedClassifiedFailure } from '../../shared/code-host-failure-copy' +import type { ClassifiedError } from '../../shared/classified-error' /** Mutate the preference-selected project so equal origin/upstream issue numbers cannot retarget edits. */ export async function updateIssue( @@ -37,14 +39,11 @@ export async function updateIssue( ) ).source if (!projectRef) { - return { - ok: false, - error: 'Could not resolve GitLab project for this repository' - } + return codeHostFailure('gitlab.projectUnresolved') } const repoFlag = projectRef.path - const errors: string[] = [] + const errors: ClassifiedError[] = [] if (updates.state) { await acquire() @@ -57,7 +56,7 @@ export async function updateIssue( } catch (err) { // glab already-state transitions exit 0; any rejected command remains a real failure. const stderr = err instanceof Error ? err.message : String(err) - errors.push(classifyGlabError(stderr).message) + errors.push(classifyGlabError(stderr)) } finally { release() } @@ -80,7 +79,7 @@ export async function updateIssue( ) } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - errors.push(classifyGlabError(stderr).message) + errors.push(classifyGlabError(stderr)) } finally { release() } @@ -120,14 +119,14 @@ export async function updateIssue( }) } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - errors.push(classifyGlabError(stderr).message) + errors.push(classifyGlabError(stderr)) } finally { release() } } if (errors.length > 0) { - return { ok: false, error: errors.join('; ') } + return combinedClassifiedFailure(errors) } return { ok: true } } diff --git a/src/main/gitlab/issues.test.ts b/src/main/gitlab/issues.test.ts index ad9b4bdbaaa..17e3dc530d7 100644 --- a/src/main/gitlab/issues.test.ts +++ b/src/main/gitlab/issues.test.ts @@ -371,7 +371,7 @@ describe('gitlab issue operations', () => { }) it('rejects createIssue with empty title', async () => { - await expect(createIssue('/repo-root', ' ', 'body')).resolves.toEqual({ + await expect(createIssue('/repo-root', ' ', 'body')).resolves.toMatchObject({ ok: false, error: 'Title is required' }) @@ -658,7 +658,7 @@ describe('gitlab issue operations', () => { it('updateIssue returns error when project ref cannot be resolved', async () => { getIssueProjectRefMock.mockResolvedValueOnce(null) - await expect(updateIssue('/repo-root', 5, { state: 'closed' })).resolves.toEqual({ + await expect(updateIssue('/repo-root', 5, { state: 'closed' })).resolves.toMatchObject({ ok: false, error: 'Could not resolve GitLab project for this repository' }) diff --git a/src/main/gitlab/issues.ts b/src/main/gitlab/issues.ts index 5d6f274793c..20c7717743b 100644 --- a/src/main/gitlab/issues.ts +++ b/src/main/gitlab/issues.ts @@ -5,6 +5,7 @@ import { mapGitLabIssueInfo } from './mappers' // prettier-ignore import { glabApiWithHeaders, glabExecFileAsync, acquire, release, getIssueProjectRef, resolveIssueSource, classifyGlabError, classifyListFetchError, getGlabKnownHosts, glabRepoExecOptions, glabHostnameArgs, parseGlabJsonList, parseGlabPaginationHeader, type LocalGitExecOptions, type ProjectRef } from './gl-utils' import { encodedProject } from './project-path-encoding' +import { codeHostFailure, classifiedFailure } from '../../shared/code-host-failure-copy' // Distinguish a confirmed empty list from fetch failure. export type IssueListResult = { @@ -125,7 +126,7 @@ export async function createIssue( ): Promise<{ ok: true; number: number; url: string } | { ok: false; error: string }> { const trimmedTitle = title.trim() if (!trimmedTitle) { - return { ok: false, error: 'Title is required' } + return codeHostFailure('titleRequired') } const knownHosts = await getGlabKnownHosts(connectionId, localGitOptions) const { source: projectRef } = await resolveIssueSource( @@ -136,10 +137,7 @@ export async function createIssue( localGitOptions ) if (!projectRef) { - return { - ok: false, - error: 'Could not resolve GitLab project for this repository' - } + return codeHostFailure('gitlab.projectUnresolved') } await acquire() try { @@ -159,7 +157,7 @@ export async function createIssue( ) const data = JSON.parse(stdout) as { iid?: number; web_url?: string; url?: string } if (typeof data.iid !== 'number') { - return { ok: false, error: 'Unexpected response from GitLab' } + return codeHostFailure('gitlab.unexpectedResponse') } return { ok: true, @@ -195,10 +193,7 @@ export async function addIssueComment( ) ).source if (!projectRef) { - return { - ok: false, - error: 'Could not resolve GitLab project for this repository' - } + return codeHostFailure('gitlab.projectUnresolved') } await acquire() try { @@ -235,7 +230,7 @@ export async function addIssueComment( return { ok: true, comment } } catch (err) { const stderr = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGlabError(stderr).message } + return classifiedFailure(classifyGlabError(stderr)) } finally { release() } diff --git a/src/main/gitlab/merge-request-review-mutations.ts b/src/main/gitlab/merge-request-review-mutations.ts index 1325d7ed31c..b3dc8432e76 100644 --- a/src/main/gitlab/merge-request-review-mutations.ts +++ b/src/main/gitlab/merge-request-review-mutations.ts @@ -18,6 +18,7 @@ import { } from './gl-utils' import { encodedProject } from './project-path-encoding' import { withProjectRef } from './merge-request-project-resolution' +import { codeHostFailure, classifiedFailure } from '../../shared/code-host-failure-copy' export async function addMRComment( repoPath: string, @@ -72,7 +73,7 @@ export async function addMRComment( release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } @@ -94,7 +95,7 @@ export async function addMRInlineComment( async (projectRef) => { const body = input.body.trim() if (!body) { - return { ok: false, error: 'Comment body is required' } + return codeHostFailure('gitlab.commentBodyRequired') } await acquire() try { @@ -154,12 +155,12 @@ export async function addMRInlineComment( } } catch (err) { const msg = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGlabError(msg).message } + return classifiedFailure(classifyGlabError(msg)) } finally { release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } @@ -182,7 +183,7 @@ export async function resolveMRDiscussion( async (projectRef) => { const trimmedDiscussionId = discussionId.trim() if (!trimmedDiscussionId) { - return { ok: false, error: 'Discussion id is required' } + return codeHostFailure('gitlab.discussionIdRequired') } await acquire() try { @@ -202,12 +203,12 @@ export async function resolveMRDiscussion( return { ok: true } } catch (err) { const msg = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGlabError(msg).message } + return classifiedFailure(classifyGlabError(msg)) } finally { release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } @@ -272,12 +273,12 @@ export async function updateMRReviewers( } } catch (err) { const msg = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGlabError(msg).message } + return classifiedFailure(classifyGlabError(msg)) } finally { release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } diff --git a/src/main/gitlab/merge-request-state-mutations.ts b/src/main/gitlab/merge-request-state-mutations.ts index 6d02f2e0755..bfb6391cd1e 100644 --- a/src/main/gitlab/merge-request-state-mutations.ts +++ b/src/main/gitlab/merge-request-state-mutations.ts @@ -9,6 +9,7 @@ import { type ProjectRef } from './gl-utils' import { withProjectRef } from './merge-request-project-resolution' +import { codeHostFailure } from '../../shared/code-host-failure-copy' // Why: glab's MRCheckErrors refuses a no-op close/reopen with these fixed texts; every other // failure, including "has already been merged", must surface. @@ -46,7 +47,7 @@ export async function closeMR( release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } @@ -82,7 +83,7 @@ export async function reopenMR( release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } @@ -120,7 +121,7 @@ export async function mergeMR( release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } diff --git a/src/main/gitlab/merge-request-update.ts b/src/main/gitlab/merge-request-update.ts index 38f73237866..670ef9d255d 100644 --- a/src/main/gitlab/merge-request-update.ts +++ b/src/main/gitlab/merge-request-update.ts @@ -13,6 +13,7 @@ import { import { encodedProject } from './project-path-encoding' import { stripGitLabDraftTitlePrefix } from './merge-request-draft-title' import { withProjectRef } from './merge-request-project-resolution' +import { codeHostFailure, classifiedFailure } from '../../shared/code-host-failure-copy' export async function updateMR( repoPath: string, @@ -41,7 +42,7 @@ export async function updateMR( await acquire() try { if (updates.readyForReview && updates.title !== undefined) { - return { ok: false, error: 'Cannot update the title while marking a merge request ready' } + return codeHostFailure('gitlab.readyTitleConflict') } const endpoint = `projects/${encodedProject(projectRef.path)}/merge_requests/${iid}` @@ -53,7 +54,7 @@ export async function updateMR( ) const currentTitle = (JSON.parse(response.stdout) as { title?: unknown }).title if (typeof currentTitle !== 'string') { - return { ok: false, error: 'Could not read the current merge request title' } + return codeHostFailure('gitlab.mergeRequestTitleUnreadable') } title = stripGitLabDraftTitlePrefix(currentTitle) ?? undefined } @@ -61,11 +62,11 @@ export async function updateMR( const fields: string[] = [] if (title !== undefined) { if (!title.trim()) { - return { ok: false, error: 'Title is required' } + return codeHostFailure('titleRequired') } fields.push(`title=${title.trim()}`) } else if (updates.title !== undefined) { - return { ok: false, error: 'Title is required' } + return codeHostFailure('titleRequired') } if (updates.body !== undefined) { fields.push(`description=${updates.body}`) @@ -96,12 +97,12 @@ export async function updateMR( return { ok: true } } catch (err) { const msg = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGlabError(msg).message } + return classifiedFailure(classifyGlabError(msg)) } finally { release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } diff --git a/src/main/gitlab/pipeline-job-mutations.ts b/src/main/gitlab/pipeline-job-mutations.ts index 394bbc1e368..bb4bbc05705 100644 --- a/src/main/gitlab/pipeline-job-mutations.ts +++ b/src/main/gitlab/pipeline-job-mutations.ts @@ -18,6 +18,7 @@ import { } from './gl-utils' import { encodedProject } from './project-path-encoding' import { withProjectRef } from './merge-request-project-resolution' +import { codeHostFailure, classifiedFailure } from '../../shared/code-host-failure-copy' // Large traces need more than the runner default; the renderer also bounds the call. const JOB_TRACE_EXEC_TIMEOUT_MS = 60_000 @@ -79,12 +80,12 @@ export async function getJobTrace( if (isMissingJobLogError(msg)) { return { ok: true, trace: '' } } - return { ok: false, error: classifyJobLogError(msg).message } + return classifiedFailure(classifyJobLogError(msg)) } finally { release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } @@ -122,12 +123,12 @@ export async function retryJob( } } catch (err) { const msg = err instanceof Error ? err.message : String(err) - return { ok: false, error: classifyGlabError(msg).message } + return classifiedFailure(classifyGlabError(msg)) } finally { release() } }, - { ok: false, error: 'Could not resolve GitLab project for this repository' }, + codeHostFailure('gitlab.projectUnresolved'), localGitOptions ) } diff --git a/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx b/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx index 6e7900f96ea..fc887182b5e 100644 --- a/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx +++ b/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx @@ -53,6 +53,7 @@ import { GHCommentComposer } from './gh-comment-composer' import { ConversationTabDescription } from './conversation-tab-description' import { ConversationTabActivity } from './conversation-tab-activity' import { ConversationTabPRSidebar } from './conversation-tab-pr-sidebar' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function ConversationTab({ item, @@ -232,7 +233,7 @@ export function ConversationTab({ if (!result.ok) { toast.error( - result.error || + codeHostErrorText(result) || translate('auto.components.GitHubItemDialog.283699bc82', 'Failed to post reply.') ) return false diff --git a/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx b/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx index e0e64b66416..b640729003e 100644 --- a/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx +++ b/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx @@ -15,6 +15,7 @@ import type { PRComment } from '../../../../../shared/github/comment-types' import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' import { translate } from '@/i18n/i18n' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function GHCommentComposer({ className, @@ -73,7 +74,7 @@ export function GHCommentComposer({ onCommentAdded(result.comment) } else { toast.error( - result.error ?? + codeHostErrorText(result) ?? translate('auto.components.GitHubItemDialog.082515176a', 'Failed to add comment') ) } diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts index 838dad736ff..3cfa9457faf 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts @@ -15,6 +15,7 @@ import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request- import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' import { translate } from '@/i18n/i18n' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type RuntimeHost = GitHubRuntimeHost | null @@ -165,7 +166,7 @@ export async function rerunGitHubChecksTab( return } if (!result.ok) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) return } toast.success( diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts index 5b7aaf96e48..359432bf0cf 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts @@ -21,6 +21,7 @@ import type { import type { TaskSourceContext } from '../../../../../shared/task-source-context' import { translate } from '@/i18n/i18n' import { loadPRFileContents } from '../load-item-details/pr-file-content-cache' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type LoadSectionArgs = { index: number @@ -277,7 +278,7 @@ export async function addPRFilesCombinedDiffLineComment({ }) if (!result.ok) { toast.error( - result.error || + codeHostErrorText(result) || translate('auto.components.GitHubItemDialog.b0b09778c8', 'Failed to add review comment.') ) return false diff --git a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx index 8586d20737b..4c88e6ba16a 100644 --- a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx +++ b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx @@ -29,6 +29,7 @@ import { runPullRequestStateUpdate } from '@/components/github/github-work-item- import type { GitHubItemDialogProjectOrigin } from '../load-item-details/github-item-dialog-types' import { PRActionsMergeMenu } from './pr-actions-merge-menu' import { WorkItemStateBadge } from '../load-item-details/work-item-state-badge' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function PRActionsPanel({ item, @@ -205,7 +206,7 @@ export function PRActionsPanel({ }) if (!result.ok) { toast.error( - result.error || + codeHostErrorText(result) || translate('auto.components.GitHubItemDialog.aba792c8b3', 'Failed to merge pull request') ) return @@ -274,7 +275,7 @@ export function PRActionsPanel({ }) if (!result.ok) { toast.error( - result.error || + codeHostErrorText(result) || (enabled ? translate( 'auto.components.GitHubItemDialog.825a8fb8cd', diff --git a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts index ff18327ed72..0bdbbd995fe 100644 --- a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts +++ b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts @@ -16,6 +16,7 @@ import type { GitHubWorkItem } from '../../../../../shared/github/work-item-type import type { TaskSourceContext } from '../../../../../shared/task-source-context' import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { translate } from '@/i18n/i18n' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type ReviewerRequestArgs = { submitting: boolean @@ -118,7 +119,7 @@ export async function requestPRReviewers({ } if (!result.ok) { toast.error( - result.error ?? + codeHostErrorText(result) ?? translate('auto.components.GitHubItemDialog.c42d942b75', 'Failed to request reviewer') ) return @@ -232,7 +233,7 @@ export async function removePRReviewers({ } if (!result.ok) { toast.error( - result.error ?? + codeHostErrorText(result) ?? translate('auto.components.GitHubItemDialog.73487fb975', 'Failed to remove reviewer') ) return diff --git a/src/renderer/src/components/github/github-work-item-edit-mutations.ts b/src/renderer/src/components/github/github-work-item-edit-mutations.ts index f5ce0709632..e76ed6d93b8 100644 --- a/src/renderer/src/components/github/github-work-item-edit-mutations.ts +++ b/src/renderer/src/components/github/github-work-item-edit-mutations.ts @@ -15,6 +15,7 @@ import { githubProjectHost } from '../../../../shared/github/project-identity' import type { TaskSourceContext } from '../../../../shared/task-source-context' import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import { codeHostErrorText } from '@/lib/code-host-error-copy' // Route Project edits to the row’s repository; callers throw on failed IPC results for optimistic rollback. export function getGitHubMutationTarget( @@ -107,7 +108,7 @@ export async function runIssueUpdate(args: { ...(args.issueRepo ? { ownerRepo: args.issueRepo } : {}) }) if (!res.ok) { - throw new Error(res.error) + throw new Error(codeHostErrorText(res)) } if (runtimeHost) { notifyWorkItemDetailsMutation( @@ -269,7 +270,7 @@ export async function runPullRequestStateUpdate(args: { updates: args.updates }) if (!res.ok) { - throw new Error(res.error) + throw new Error(codeHostErrorText(res)) } if (target.kind === 'environment') { notifyWorkItemDetailsMutation( diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts index 063a1b1c548..14917b8f233 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts @@ -12,6 +12,7 @@ import { } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function useGitLabDetailsEditing( item: GitLabWorkItem | null, @@ -154,7 +155,7 @@ export function useGitLabDetailsEditing( useAppStore.getState().recordFeatureInteraction('gitlab-tasks') } } else if (mountedRef.current) { - toast.error(res.error) + toast.error(codeHostErrorText(res)) } } catch (error) { if (mountedRef.current) { diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts index e317e262ca3..07ab67d72d5 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts @@ -6,6 +6,7 @@ import type { GitLabPipelineJob, GitLabWorkItem } from '../../../../shared/gitla import { showGitLabMutationError } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function useGitLabPipelineActions( item: GitLabWorkItem | null, @@ -50,7 +51,7 @@ export function useGitLabPipelineActions( ...current, [job.id]: result.ok ? { loading: false, trace: result.trace } - : { loading: false, error: result.error } + : { loading: false, error: codeHostErrorText(result) } })) } catch (error) { if (mountedRef.current) { @@ -111,7 +112,7 @@ export function useGitLabPipelineActions( } handleRefresh() } else { - toast.error(result.error) + toast.error(codeHostErrorText(result)) } } catch (error) { if (mountedRef.current) { diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts index b14cdda649a..958615e6bc1 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts @@ -8,6 +8,7 @@ import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { showGitLabMutationError } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function useGitLabPrimaryActions( item: GitLabWorkItem | null, @@ -42,7 +43,7 @@ export function useGitLabPrimaryActions( } } else { if (mountedRef.current) { - toast.error(res.error) + toast.error(codeHostErrorText(res)) } } } catch (error) { @@ -75,7 +76,7 @@ export function useGitLabPrimaryActions( } } else { if (mountedRef.current) { - toast.error(res.error) + toast.error(codeHostErrorText(res)) } } } catch (error) { @@ -108,7 +109,7 @@ export function useGitLabPrimaryActions( } } else { if (mountedRef.current) { - toast.error(res.error) + toast.error(codeHostErrorText(res)) } } } catch (error) { @@ -160,7 +161,7 @@ export function useGitLabPrimaryActions( } } else { if (mountedRef.current) { - toast.error(res.error) + toast.error(codeHostErrorText(res)) } } } catch (error) { diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts index 18b1c30cb30..4a387df5611 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts @@ -8,6 +8,7 @@ import type { GitLabAssignableUser, GitLabWorkItem } from '../../../../shared/gi import { dedupeGitLabUsers, showGitLabMutationError } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function useGitLabReviewActions( item: GitLabWorkItem | null, @@ -97,7 +98,7 @@ export function useGitLabReviewActions( ) useAppStore.getState().recordFeatureInteraction('gitlab-tasks') } else { - toast.error(result.error) + toast.error(codeHostErrorText(result)) } } catch (error) { if (mountedRef.current) { @@ -184,7 +185,7 @@ export function useGitLabReviewActions( translate('auto.components.GitLabItemDialog.60c13320c4', 'Inline comment added') ) } else { - toast.error(result.error) + toast.error(codeHostErrorText(result)) } } catch (error) { if (mountedRef.current) { @@ -236,7 +237,7 @@ export function useGitLabReviewActions( useAppStore.getState().recordFeatureInteraction('gitlab-tasks') } } else if (mountedRef.current) { - toast.error(res.error) + toast.error(codeHostErrorText(res)) } } catch (error) { if (mountedRef.current) { diff --git a/src/renderer/src/components/pull-request-page/actions/merge-actions.ts b/src/renderer/src/components/pull-request-page/actions/merge-actions.ts index 47f49379b4e..b4981aac4cf 100644 --- a/src/renderer/src/components/pull-request-page/actions/merge-actions.ts +++ b/src/renderer/src/components/pull-request-page/actions/merge-actions.ts @@ -19,6 +19,7 @@ import type { TaskSourceContext } from '../../../../../shared/task-source-contex import type { PullRequestPageProjectOrigin } from '../page-types' import { translate } from '@/i18n/i18n' import type { GitHubPRMergeStatePresentation } from '@/components/github-pr-merge-state' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export async function changePullRequestState(args: { canMutateState: boolean @@ -163,7 +164,7 @@ export async function mergePullRequest(args: { prRepo: args.prRepo }) if (!result.ok) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) return } args.applyStatePatch('merged') @@ -235,7 +236,7 @@ export async function setPullRequestAutoMerge(args: { prRepo: args.prRepo }) if (!result.ok) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) return } if (args.mergeTarget.kind === 'environment') { diff --git a/src/renderer/src/components/pull-request-page/checks/rerun.ts b/src/renderer/src/components/pull-request-page/checks/rerun.ts index 6f8eaec76a7..98d9f60298c 100644 --- a/src/renderer/src/components/pull-request-page/checks/rerun.ts +++ b/src/renderer/src/components/pull-request-page/checks/rerun.ts @@ -7,6 +7,7 @@ import type { GitHubWorkItem } from '../../../../../shared/github/work-item-type import type { PRCheckDetail } from '../../../../../shared/github/check-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' import type { GitHubChecksTabState } from '../../github-checks-tab-state' +import { codeHostErrorText } from '@/lib/code-host-error-copy' /** The checks tab mints one of these per context; only its reference identity is ever read. */ type ChecksContextOwner = GitHubChecksTabState['contextOwner'] @@ -67,7 +68,7 @@ export async function rerunPullRequestChecks(args: { return } if (!result.ok) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) return } toast.success( diff --git a/src/renderer/src/components/pull-request-page/comments/composer.tsx b/src/renderer/src/components/pull-request-page/comments/composer.tsx index 8eab0da3aff..e10ec30d21a 100644 --- a/src/renderer/src/components/pull-request-page/comments/composer.tsx +++ b/src/renderer/src/components/pull-request-page/comments/composer.tsx @@ -17,6 +17,7 @@ import type { PRComment } from '../../../../../shared/github/comment-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' import type { MentionOption } from '../page-types' import { MentionTextarea } from '../mentions/textarea' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function GHCommentComposer({ className, @@ -88,7 +89,7 @@ export function GHCommentComposer({ onCommentAdded(result.comment) } else { toast.error( - result.error ?? + codeHostErrorText(result) ?? translate('auto.components.PullRequestPage.1208347ac0', 'Failed to add comment') ) } diff --git a/src/renderer/src/components/pull-request-page/conversation/reply.ts b/src/renderer/src/components/pull-request-page/conversation/reply.ts index b18119ac3ed..e0490f85458 100644 --- a/src/renderer/src/components/pull-request-page/conversation/reply.ts +++ b/src/renderer/src/components/pull-request-page/conversation/reply.ts @@ -13,6 +13,7 @@ import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request- import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { PRComment } from '../../../../../shared/github/comment-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export async function postConversationReply(args: { canUseRepoMutationContext: boolean @@ -62,7 +63,7 @@ export async function postConversationReply(args: { if (!result.ok) { toast.error( - result.error || + codeHostErrorText(result) || translate('auto.components.PullRequestPage.5821aab360', 'Failed to post reply.') ) return false diff --git a/src/renderer/src/components/pull-request-page/files/line-comment.ts b/src/renderer/src/components/pull-request-page/files/line-comment.ts index d3e719ae255..bff653e43e7 100644 --- a/src/renderer/src/components/pull-request-page/files/line-comment.ts +++ b/src/renderer/src/components/pull-request-page/files/line-comment.ts @@ -5,6 +5,7 @@ import type { DiffSection } from '@/components/editor/diff-section-types' import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { PRComment } from '../../../../../shared/github/comment-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export async function addPullRequestLineComment(args: { headSha: string | undefined @@ -52,7 +53,7 @@ export async function addPullRequestLineComment(args: { } if (!result.ok) { toast.error( - result.error || + codeHostErrorText(result) || translate('auto.components.PullRequestPage.19628e058d', 'Failed to add review comment.') ) return false diff --git a/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts b/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts index 758ea4ac6a9..48e3af9b3e1 100644 --- a/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts +++ b/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts @@ -15,6 +15,7 @@ import type { import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' +import { codeHostErrorText } from '@/lib/code-host-error-copy' const MAX_REQUESTED_REVIEWERS = 15 @@ -110,7 +111,7 @@ export function createReviewerRequestActions(args: ReviewerRequestActionsArgs): } if (!result.ok) { toast.error( - result.error ?? + codeHostErrorText(result) ?? translate('auto.components.PullRequestPage.2560588245', 'Failed to request reviewer') ) return @@ -211,7 +212,7 @@ export function createReviewerRequestActions(args: ReviewerRequestActionsArgs): } if (!result.ok) { toast.error( - result.error ?? + codeHostErrorText(result) ?? translate('auto.components.PullRequestPage.c798fa0ec7', 'Failed to remove reviewer') ) return diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-ai-acknowledgement.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-ai-acknowledgement.tsx index 1703a53d83c..068e83f732f 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-ai-acknowledgement.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-ai-acknowledgement.tsx @@ -21,6 +21,7 @@ import { clearPRCommentsListSelection } from '../pr-comments-list-selection' import { translate } from '@/i18n/i18n' import type { ChecksAgentComposerState } from './panel-state-types' import type { ChecksPanelReview } from '../checks-panel-review' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type ChecksPanelAiAcknowledgementInput = Pick< ChecksPanelControllerState, @@ -168,7 +169,7 @@ export function useChecksPanelAiAcknowledgement(model: ChecksPanelAiAcknowledgem } return true } - lastHostError = result.error + lastHostError = codeHostErrorText(result) console.warn('In-thread fixing reply failed:', result.error) return false } catch (err) { @@ -198,7 +199,7 @@ export function useChecksPanelAiAcknowledgement(model: ChecksPanelAiAcknowledgem } return true } - lastHostError = result.error + lastHostError = codeHostErrorText(result) console.warn('Conversation fixing reply failed:', result.error) return false } catch (err) { diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-mutations.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-mutations.tsx index 1df507c3da9..987d569cbbb 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-mutations.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-mutations.tsx @@ -17,6 +17,7 @@ import { checksPanelAsyncResultKey } from '../checks-panel-async-result-key' import { isMutablePRConversationComment } from '../../../../../shared/github/pr-comment-mutability' import type { GitHubReactionContent, PRComment } from '../../../../../shared/github/comment-types' import { translate } from '@/i18n/i18n' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type ChecksPanelCommentMutationsInput = Pick< ChecksPanelControllerState, @@ -69,7 +70,7 @@ export function useChecksPanelCommentMutations(model: ChecksPanelCommentMutation return result.ok ? { ok: true as const } : result } if (!result.ok) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) return result } setComments((prev) => mergePRCommentIntoList(prev, result.comment)) @@ -238,7 +239,7 @@ export function useChecksPanelCommentMutations(model: ChecksPanelCommentMutation } if (!result.ok) { if (notifyOnFailure) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) } return result } diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx index 11d7c14bfad..93c0d1743e9 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx @@ -23,6 +23,7 @@ import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { isReviewThreadResolutionInFlight } from '@/store/github/review-thread-resolution-in-flight' import { worktreeLaunchWorkspace } from '@/lib/agent-launch-route-input' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type ChecksPanelCommentResolutionInput = Pick< ChecksPanelControllerState, @@ -98,7 +99,7 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut if (!result.ok) { rollbackThread(previousThreadComments) if (notifyOnFailure) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) } return false } diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-entry-refresh-and-title-actions.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-entry-refresh-and-title-actions.tsx index 1b6900a99ca..ccf19e03e2b 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-entry-refresh-and-title-actions.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-entry-refresh-and-title-actions.tsx @@ -10,6 +10,7 @@ import type { ChecksPanelControllerState } from './use-checks-panel-controller-s import type { ChecksPanelReviewState } from './use-checks-panel-review-state' import type { ChecksPanelPollingState } from './use-checks-panel-polling' import type { ChecksPanelReviewDataState } from './use-checks-panel-review-data' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type ChecksPanelEntryRefreshAndTitleActionsInput = Pick< ChecksPanelContextState, @@ -308,7 +309,7 @@ export function useChecksPanelEntryRefreshAndTitleActions( updates: { title: nextTitle } }) if (!result.ok) { - toast.error(result.error) + toast.error(codeHostErrorText(result)) return } await refreshHostedReviewAfterMutation() diff --git a/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx b/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx index ac93268200d..c9c33bd4059 100644 --- a/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx +++ b/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx @@ -15,6 +15,7 @@ import { scheduleRightPanelCommentFocusTimer } from './right-panel-comment-focus-timers' import { translate } from '@/i18n/i18n' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export type RightPanelCommentSubmitResult = { ok: true } | { ok: false; error: string } @@ -160,7 +161,7 @@ export function RightPanelCommentComposer({ setBody('') onCancel?.() } else { - setError(result.error) + setError(codeHostErrorText(result)) } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to post comment.') diff --git a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts index c0bff341151..b302d732488 100644 --- a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts +++ b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts @@ -16,6 +16,7 @@ import { translate } from '@/i18n/i18n' import { getHostedReviewStateChangeCopy } from './hosted-review-state-change-copy' import { buildGitHubPRStackMergeConfirmation } from './github-pr-stack-confirmation' import { useReadyHostedReviewAction } from './use-ready-hosted-review-action' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export type HostedReviewActionInfo = Pick< HostedReviewInfo, @@ -115,7 +116,7 @@ export function useHostedReviewActions({ prRepo: githubPR?.prRepo ?? null }) if (!result.ok) { - setActionError(result.error) + setActionError(codeHostErrorText(result)) } else { await onRefreshReview() } @@ -155,7 +156,7 @@ export function useHostedReviewActions({ prRepo: githubPR?.prRepo ?? null }) if (!result.ok) { - setActionError(result.error) + setActionError(codeHostErrorText(result)) // Refresh stale eligibility without replacing the provider's mutation error. await onRefreshReview().catch(() => {}) } else { @@ -221,8 +222,9 @@ export function useHostedReviewActions({ nextState }) if (!result.ok) { - setActionError(result.error) - toast.error(result.error) + const message = codeHostErrorText(result) + setActionError(message) + toast.error(message) } else { toast.success( isClosing diff --git a/src/renderer/src/components/right-sidebar/use-ready-hosted-review-action.ts b/src/renderer/src/components/right-sidebar/use-ready-hosted-review-action.ts index 25accaf1f5e..60ec42742d8 100644 --- a/src/renderer/src/components/right-sidebar/use-ready-hosted-review-action.ts +++ b/src/renderer/src/components/right-sidebar/use-ready-hosted-review-action.ts @@ -5,6 +5,7 @@ import type { Repo } from '../../../../shared/repo-types' import { translate } from '@/i18n/i18n' import { markGitHubHostedReviewReadyForReview } from './hosted-review-github-actions' import { markGitLabHostedReviewReadyForReview } from './hosted-review-gitlab-actions' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function useReadyHostedReviewAction({ reviewNumber, @@ -44,8 +45,9 @@ export function useReadyHostedReviewAction({ prRepo: githubPR?.prRepo ?? null }) if (!result.ok) { - setActionError(result.error) - toast.error(result.error) + const message = codeHostErrorText(result) + setActionError(message) + toast.error(message) return } toast.success( diff --git a/src/renderer/src/components/task-page/github/AssigneesCell.tsx b/src/renderer/src/components/task-page/github/AssigneesCell.tsx index 8747944e95a..b797f231b5c 100644 --- a/src/renderer/src/components/task-page/github/AssigneesCell.tsx +++ b/src/renderer/src/components/task-page/github/AssigneesCell.tsx @@ -17,6 +17,7 @@ import { LoaderCircle, ChevronDown, Check } from 'lucide-react' import type { TaskPageGitHubWorkItemMutationRunner } from '../linear/task-page-linear-jira-list-model' import { GitHubAssigneeAvatar } from './Avatars' import { GitHubUserAvatar } from '@/components/github/github-user-avatar' +import { codeHostErrorText } from '@/lib/code-host-error-copy' export function GHAssigneesCell({ item, repo, @@ -142,7 +143,7 @@ export function GHAssigneesCell({ updates }) if (res && res.ok === false) { - throw new Error(res.error) + throw new Error(codeHostErrorText(res)) } return res } diff --git a/src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.ts b/src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.ts index c6e476bc206..aa0e8cd1e52 100644 --- a/src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.ts +++ b/src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.ts @@ -5,6 +5,7 @@ import { useAppStore } from '@/store/index' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' +import { codeHostErrorText } from '@/lib/code-host-error-copy' type TaskPageGitHubIssueCreationInput = Pick< TaskPageWorkspaceActionsModel, | 'setTaskRefreshNonce' @@ -92,7 +93,7 @@ export function useTaskPageGitHubIssueCreation Promise<{ ok?: boolean; error?: string | { message?: string } } | void> + mutate: () => Promise<{ + ok?: boolean + error?: string | { message?: string } + errorCode?: unknown + errorDetail?: unknown + } | void> successToast?: string errorToast: string serverEntityFromResult?: (result: unknown) => Partial | undefined @@ -128,7 +134,7 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte try { const result = await input.mutate() const activeQuery = activeQueryRef.current - const typed = result as { ok?: boolean; error?: string | { message?: string } } | void + const typed = result if (typed && typeof typed === 'object' && typed.ok === false) { const rolled = rollbackTaskPageGitHubWorkItemMutation({ key: began.key, @@ -143,7 +149,7 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte if (rolled === 'rolled_back' && mountedRef.current) { const message = typeof typed.error === 'string' - ? typed.error + ? codeHostErrorText({ ...typed, error: typed.error }) : (typed.error?.message ?? input.errorToast) toast.error(message) } diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index de6077097ee..0c080428aec 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1238,6 +1238,54 @@ "manyOtherSites_other": "Read and change your data on {{hosts}} and {{count}} other sites" } }, + "codeHost": { + "errors": { + "github": { + "autoMergeAlreadyMergeable": "This pull request can already be merged. Use Merge instead of auto-merge.", + "autoMergeDisableFailed": "Failed to disable auto-merge for pull request: {{detail}}", + "autoMergeEnableFailed": "Failed to enable auto-merge for pull request: {{detail}}", + "issuePermissionDenied": "You don't have permission to edit this issue. Check your GitHub token scopes.", + "issuesDisabled": "Issues are disabled on this repository.", + "mergeQueueAddFailed": "Failed to add pull request to the merge queue: {{detail}}", + "noFailedChecks": "No failed GitHub Actions checks to rerun.", + "noRerunnableChecks": "No rerunnable checks found.", + "pullRequestIdUnresolved": "Could not resolve GitHub pull request ID", + "pullRequestNotFound": "Pull request not found — it may have been deleted.", + "pullRequestPermissionDenied": "You don't have permission to edit this pull request. Check your GitHub token scopes.", + "pullRequestUpdateFailed": "Failed to update pull request: {{detail}}", + "rateLimited": "GitHub rate limit hit. Try again in a few minutes.", + "repoUnresolved": "Could not resolve GitHub owner/repo for this repository", + "rerunFailed": "Failed to rerun checks: {{detail}}", + "rerunPermissionDenied": "You don't have permission to rerun checks on this repository. Check your GitHub token scopes.", + "rerunRejected": "Could not rerun checks — {{detail}}", + "rerunTargetNotFound": "GitHub resource to rerun was not found — it may have expired or been deleted.", + "reviewerRequired": "Enter at least one reviewer", + "stackAutoMergeUnsupported": "GitHub does not support auto-merge for stacked pull requests.", + "stackMergeInvalidResponse": "GitHub returned an invalid stack merge response.", + "stackMergePending": "GitHub is still merging this stack. Refresh to check its status.", + "unexpectedResponse": "Unexpected response from GitHub" + }, + "gitlab": { + "commentBodyRequired": "Comment body is required", + "discussionIdRequired": "Discussion id is required", + "issuePermissionDenied": "You don't have permission to edit this issue. Check your GitLab token scopes.", + "jobLogFailed": "Failed to load the job log: {{detail}}", + "jobLogPermissionDenied": "You don't have permission to read this job's log. Check your GitLab token scopes.", + "jobProjectNotFound": "Could not find this job's GitLab project.", + "mergeRequestTitleUnreadable": "Could not read the current merge request title", + "projectUnresolved": "Could not resolve GitLab project for this repository", + "rateLimited": "GitLab rate limit hit. Try again in a few minutes.", + "readyTitleConflict": "Cannot update the title while marking a merge request ready", + "unexpectedResponse": "Unexpected response from GitLab" + }, + "invalidRequest": "Invalid request — {{detail}}", + "invalidUpdate": "Invalid update — {{detail}}", + "issueNotFound": "Issue not found — it may have been deleted.", + "issueUpdateFailed": "Failed to update issue: {{detail}}", + "networkError": "Network error — check your connection.", + "titleRequired": "Title is required" + } + }, "components": { "native-chat": { "approval": { diff --git a/src/renderer/src/i18n/locales/de.json b/src/renderer/src/i18n/locales/de.json index 1944db9cbd6..3462545725f 100644 --- a/src/renderer/src/i18n/locales/de.json +++ b/src/renderer/src/i18n/locales/de.json @@ -21802,5 +21802,53 @@ "unsupported": "Der ausgewählte Automatisierungs-Host unterstützt dieses Projekt nicht." } } + }, + "codeHost": { + "errors": { + "titleRequired": "Titel ist erforderlich", + "networkError": "Netzwerkfehler — Verbindung überprüfen.", + "invalidUpdate": "Ungültige Aktualisierung — {{detail}}", + "invalidRequest": "Ungültige Anfrage — {{detail}}", + "issueNotFound": "Issue nicht gefunden — es wurde möglicherweise gelöscht.", + "issueUpdateFailed": "Issue konnte nicht aktualisiert werden: {{detail}}", + "github": { + "repoUnresolved": "owner/repo für dieses Repository auf GitHub konnte nicht ermittelt werden", + "unexpectedResponse": "Unerwartete Antwort von GitHub", + "pullRequestIdUnresolved": "GitHub-Pull-Request-ID konnte nicht ermittelt werden", + "reviewerRequired": "Mindestens einen Prüfer eingeben", + "stackAutoMergeUnsupported": "GitHub unterstützt keinen automatischen Merge für Pull Requests in einem Stack.", + "autoMergeAlreadyMergeable": "Dieser Pull Request kann bereits gemergt werden. Statt automatischem Merge „Mergen“ verwenden.", + "noFailedChecks": "Keine fehlgeschlagenen Checks von GitHub Actions zum erneuten Ausführen.", + "noRerunnableChecks": "Keine erneut ausführbaren Checks gefunden.", + "stackMergeInvalidResponse": "GitHub hat eine ungültige Antwort zum Stack-Merge zurückgegeben.", + "stackMergePending": "GitHub mergt diesen Stack noch. Zum Prüfen des Status aktualisieren.", + "rateLimited": "GitHub-Ratenlimit erreicht. In einigen Minuten erneut versuchen.", + "issuePermissionDenied": "Keine Berechtigung zum Bearbeiten dieses Issues. GitHub-Token-Berechtigungen prüfen.", + "issuesDisabled": "Issues sind für dieses Repository deaktiviert.", + "pullRequestPermissionDenied": "Keine Berechtigung zum Bearbeiten dieses Pull Requests. GitHub-Token-Berechtigungen prüfen.", + "pullRequestNotFound": "Pull Request nicht gefunden — er wurde möglicherweise gelöscht.", + "pullRequestUpdateFailed": "Pull Request konnte nicht aktualisiert werden: {{detail}}", + "autoMergeEnableFailed": "Automatischer Merge für den Pull Request konnte nicht aktiviert werden: {{detail}}", + "autoMergeDisableFailed": "Automatischer Merge für den Pull Request konnte nicht deaktiviert werden: {{detail}}", + "mergeQueueAddFailed": "Pull Request konnte nicht zur Merge-Warteschlange hinzugefügt werden: {{detail}}", + "rerunPermissionDenied": "Keine Berechtigung zum erneuten Ausführen von Checks in diesem Repository. GitHub-Token-Berechtigungen prüfen.", + "rerunTargetNotFound": "GitHub-Ressource zum erneuten Ausführen nicht gefunden — sie ist möglicherweise abgelaufen oder wurde gelöscht.", + "rerunRejected": "Checks konnten nicht erneut ausgeführt werden — {{detail}}", + "rerunFailed": "Checks konnten nicht erneut ausgeführt werden: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "GitLab-Projekt für dieses Repository konnte nicht ermittelt werden", + "unexpectedResponse": "Unerwartete Antwort von GitLab", + "commentBodyRequired": "Kommentartext ist erforderlich", + "discussionIdRequired": "Diskussions-ID ist erforderlich", + "readyTitleConflict": "Beim Markieren eines Merge Requests als bereit kann der Titel nicht aktualisiert werden", + "mergeRequestTitleUnreadable": "Aktueller Merge-Request-Titel konnte nicht gelesen werden", + "rateLimited": "GitLab-Ratenlimit erreicht. In einigen Minuten erneut versuchen.", + "issuePermissionDenied": "Keine Berechtigung zum Bearbeiten dieses Issues. GitLab-Token-Berechtigungen prüfen.", + "jobLogPermissionDenied": "Keine Berechtigung zum Lesen dieses Job-Protokolls. GitLab-Token-Berechtigungen prüfen.", + "jobProjectNotFound": "GitLab-Projekt dieses Jobs konnte nicht gefunden werden.", + "jobLogFailed": "Job-Protokoll konnte nicht geladen werden: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index c47fe714490..fd509d8b815 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -22224,5 +22224,53 @@ "unsupported": "The selected automation host does not support this project." } } + }, + "codeHost": { + "errors": { + "titleRequired": "Title is required", + "networkError": "Network error — check your connection.", + "invalidUpdate": "Invalid update — {{detail}}", + "invalidRequest": "Invalid request — {{detail}}", + "issueNotFound": "Issue not found — it may have been deleted.", + "issueUpdateFailed": "Failed to update issue: {{detail}}", + "github": { + "repoUnresolved": "Could not resolve GitHub owner/repo for this repository", + "unexpectedResponse": "Unexpected response from GitHub", + "pullRequestIdUnresolved": "Could not resolve GitHub pull request ID", + "reviewerRequired": "Enter at least one reviewer", + "stackAutoMergeUnsupported": "GitHub does not support auto-merge for stacked pull requests.", + "autoMergeAlreadyMergeable": "This pull request can already be merged. Use Merge instead of auto-merge.", + "noFailedChecks": "No failed GitHub Actions checks to rerun.", + "noRerunnableChecks": "No rerunnable checks found.", + "stackMergeInvalidResponse": "GitHub returned an invalid stack merge response.", + "stackMergePending": "GitHub is still merging this stack. Refresh to check its status.", + "rateLimited": "GitHub rate limit hit. Try again in a few minutes.", + "issuePermissionDenied": "You don't have permission to edit this issue. Check your GitHub token scopes.", + "issuesDisabled": "Issues are disabled on this repository.", + "pullRequestPermissionDenied": "You don't have permission to edit this pull request. Check your GitHub token scopes.", + "pullRequestNotFound": "Pull request not found — it may have been deleted.", + "pullRequestUpdateFailed": "Failed to update pull request: {{detail}}", + "autoMergeEnableFailed": "Failed to enable auto-merge for pull request: {{detail}}", + "autoMergeDisableFailed": "Failed to disable auto-merge for pull request: {{detail}}", + "mergeQueueAddFailed": "Failed to add pull request to the merge queue: {{detail}}", + "rerunPermissionDenied": "You don't have permission to rerun checks on this repository. Check your GitHub token scopes.", + "rerunTargetNotFound": "GitHub resource to rerun was not found — it may have expired or been deleted.", + "rerunRejected": "Could not rerun checks — {{detail}}", + "rerunFailed": "Failed to rerun checks: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "Could not resolve GitLab project for this repository", + "unexpectedResponse": "Unexpected response from GitLab", + "commentBodyRequired": "Comment body is required", + "discussionIdRequired": "Discussion id is required", + "readyTitleConflict": "Cannot update the title while marking a merge request ready", + "mergeRequestTitleUnreadable": "Could not read the current merge request title", + "rateLimited": "GitLab rate limit hit. Try again in a few minutes.", + "issuePermissionDenied": "You don't have permission to edit this issue. Check your GitLab token scopes.", + "jobLogPermissionDenied": "You don't have permission to read this job's log. Check your GitLab token scopes.", + "jobProjectNotFound": "Could not find this job's GitLab project.", + "jobLogFailed": "Failed to load the job log: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index ddbcfb77840..233dd64c287 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -22419,5 +22419,53 @@ "unsupported": "El host seleccionado para la automatización no admite este proyecto." } } + }, + "codeHost": { + "errors": { + "titleRequired": "El título es obligatorio", + "networkError": "Error de red — comprueba tu conexión.", + "invalidUpdate": "Actualización no válida — {{detail}}", + "invalidRequest": "Solicitud no válida — {{detail}}", + "issueNotFound": "No se encontró el issue — puede que se haya eliminado.", + "issueUpdateFailed": "No se pudo actualizar el issue: {{detail}}", + "github": { + "repoUnresolved": "No se pudo determinar el owner/repo de GitHub para este repositorio", + "unexpectedResponse": "Respuesta inesperada de GitHub", + "pullRequestIdUnresolved": "No se pudo determinar el ID del pull request de GitHub", + "reviewerRequired": "Introduce al menos un revisor", + "stackAutoMergeUnsupported": "GitHub no admite auto-merge para pull requests apilados.", + "autoMergeAlreadyMergeable": "Este pull request ya se puede mergear. Usa Merge en lugar de auto-merge.", + "noFailedChecks": "No hay checks fallidos de GitHub Actions para volver a ejecutar.", + "noRerunnableChecks": "No se encontraron checks que se puedan volver a ejecutar.", + "stackMergeInvalidResponse": "GitHub devolvió una respuesta no válida al mergear la pila.", + "stackMergePending": "GitHub aún está mergeando esta pila. Actualiza para consultar su estado.", + "rateLimited": "Se alcanzó el límite de velocidad de GitHub. Inténtalo de nuevo en unos minutos.", + "issuePermissionDenied": "No tienes permiso para editar este issue. Comprueba los alcances de tu token de GitHub.", + "issuesDisabled": "Los issues están deshabilitados en este repositorio.", + "pullRequestPermissionDenied": "No tienes permiso para editar este pull request. Comprueba los alcances de tu token de GitHub.", + "pullRequestNotFound": "No se encontró el pull request — puede que se haya eliminado.", + "pullRequestUpdateFailed": "No se pudo actualizar el pull request: {{detail}}", + "autoMergeEnableFailed": "No se pudo habilitar auto-merge para el pull request: {{detail}}", + "autoMergeDisableFailed": "No se pudo deshabilitar auto-merge para el pull request: {{detail}}", + "mergeQueueAddFailed": "No se pudo añadir el pull request a la merge queue: {{detail}}", + "rerunPermissionDenied": "No tienes permiso para volver a ejecutar checks en este repositorio. Comprueba los alcances de tu token de GitHub.", + "rerunTargetNotFound": "No se encontró el recurso de GitHub que se iba a volver a ejecutar — puede que haya caducado o se haya eliminado.", + "rerunRejected": "No se pudieron volver a ejecutar los checks — {{detail}}", + "rerunFailed": "No se pudieron volver a ejecutar los checks: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "No se pudo determinar el proyecto de GitLab para este repositorio", + "unexpectedResponse": "Respuesta inesperada de GitLab", + "commentBodyRequired": "El contenido del comentario es obligatorio", + "discussionIdRequired": "El ID de la discusión es obligatorio", + "readyTitleConflict": "No se puede actualizar el título al marcar un merge request como listo", + "mergeRequestTitleUnreadable": "No se pudo leer el título actual del merge request", + "rateLimited": "Se alcanzó el límite de velocidad de GitLab. Inténtalo de nuevo en unos minutos.", + "issuePermissionDenied": "No tienes permiso para editar este issue. Comprueba los alcances de tu token de GitLab.", + "jobLogPermissionDenied": "No tienes permiso para leer el log de este job. Comprueba los alcances de tu token de GitLab.", + "jobProjectNotFound": "No se pudo encontrar el proyecto de GitLab de este job.", + "jobLogFailed": "No se pudo cargar el log del job: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 7f705d38f70..48ad57eb8b3 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -22419,5 +22419,53 @@ "unsupported": "L’hôte sélectionné pour l’automatisation ne prend pas en charge ce projet." } } + }, + "codeHost": { + "errors": { + "titleRequired": "Le titre est obligatoire", + "networkError": "Erreur réseau — vérifiez votre connexion.", + "invalidUpdate": "Mise à jour invalide — {{detail}}", + "invalidRequest": "Requête invalide — {{detail}}", + "issueNotFound": "Issue introuvable — elle a peut-être été supprimée.", + "issueUpdateFailed": "Échec de la mise à jour de l'issue : {{detail}}", + "github": { + "repoUnresolved": "Impossible de déterminer le propriétaire et le nom de ce dépôt sur GitHub", + "unexpectedResponse": "Réponse inattendue de GitHub", + "pullRequestIdUnresolved": "Impossible de déterminer l'ID de la pull request GitHub", + "reviewerRequired": "Saisissez au moins un reviewer", + "stackAutoMergeUnsupported": "GitHub ne prend pas en charge l'auto-merge des pull requests empilées.", + "autoMergeAlreadyMergeable": "Cette pull request peut déjà être mergée. Utilisez Merge au lieu de l'auto-merge.", + "noFailedChecks": "Aucune vérification GitHub Actions en échec à relancer.", + "noRerunnableChecks": "Aucune vérification relançable trouvée.", + "stackMergeInvalidResponse": "GitHub a renvoyé une réponse invalide pour le merge de la stack.", + "stackMergePending": "GitHub est encore en train de merger cette stack. Actualisez pour vérifier son état.", + "rateLimited": "Limite de débit GitHub atteinte. Réessayez dans quelques minutes.", + "issuePermissionDenied": "Vous n'avez pas l'autorisation de modifier cette issue. Vérifiez les scopes de votre token GitHub.", + "issuesDisabled": "Les issues sont désactivées sur ce dépôt.", + "pullRequestPermissionDenied": "Vous n'avez pas l'autorisation de modifier cette pull request. Vérifiez les scopes de votre token GitHub.", + "pullRequestNotFound": "Pull request introuvable — elle a peut-être été supprimée.", + "pullRequestUpdateFailed": "Échec de la mise à jour de la pull request : {{detail}}", + "autoMergeEnableFailed": "Échec de l'activation de l'auto-merge pour la pull request : {{detail}}", + "autoMergeDisableFailed": "Échec de la désactivation de l'auto-merge pour la pull request : {{detail}}", + "mergeQueueAddFailed": "Échec de l'ajout de la pull request à la merge queue : {{detail}}", + "rerunPermissionDenied": "Vous n'avez pas l'autorisation de relancer les vérifications sur ce dépôt. Vérifiez les scopes de votre token GitHub.", + "rerunTargetNotFound": "Ressource GitHub à relancer introuvable — elle a peut-être expiré ou été supprimée.", + "rerunRejected": "Impossible de relancer les vérifications — {{detail}}", + "rerunFailed": "Échec de la relance des vérifications : {{detail}}" + }, + "gitlab": { + "projectUnresolved": "Impossible de déterminer le projet GitLab pour ce dépôt", + "unexpectedResponse": "Réponse inattendue de GitLab", + "commentBodyRequired": "Le contenu du commentaire est requis", + "discussionIdRequired": "L'ID de la discussion est requis", + "readyTitleConflict": "Impossible de modifier le titre en marquant une merge request comme prête", + "mergeRequestTitleUnreadable": "Impossible de lire le titre actuel de la merge request", + "rateLimited": "Limite de débit GitLab atteinte. Réessayez dans quelques minutes.", + "issuePermissionDenied": "Vous n'avez pas l'autorisation de modifier cette issue. Vérifiez les scopes de votre token GitLab.", + "jobLogPermissionDenied": "Vous n'avez pas l'autorisation de lire le journal de ce job. Vérifiez les scopes de votre token GitLab.", + "jobProjectNotFound": "Impossible de trouver le projet GitLab de ce job.", + "jobLogFailed": "Échec du chargement du journal du job : {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 801a9834990..d7f08dde066 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -21805,5 +21805,53 @@ "unsupported": "選択したオートメーションのホストは、このプロジェクトをサポートしていません。" } } + }, + "codeHost": { + "errors": { + "titleRequired": "タイトルは必須です", + "networkError": "ネットワークエラー — 接続を確認してください。", + "invalidUpdate": "無効な更新 — {{detail}}", + "invalidRequest": "無効なリクエスト — {{detail}}", + "issueNotFound": "Issue が見つかりません — 削除された可能性があります。", + "issueUpdateFailed": "Issue を更新できませんでした: {{detail}}", + "github": { + "repoUnresolved": "このリポジトリの GitHub 所有者とリポジトリ名を特定できませんでした", + "unexpectedResponse": "GitHub から予期しない応答がありました", + "pullRequestIdUnresolved": "GitHub PR の ID を特定できませんでした", + "reviewerRequired": "1 人以上のレビュアーを入力してください", + "stackAutoMergeUnsupported": "GitHub はスタックされた PR の自動マージに対応していません。", + "autoMergeAlreadyMergeable": "この PR はすでにマージ可能です。自動マージではなく「マージ」を使用してください。", + "noFailedChecks": "再実行対象の失敗した GitHub Actions チェックはありません。", + "noRerunnableChecks": "再実行できるチェックが見つかりませんでした。", + "stackMergeInvalidResponse": "GitHub がスタックのマージに対して無効な応答を返しました。", + "stackMergePending": "GitHub はまだこのスタックをマージ中です。更新して状態を確認してください。", + "rateLimited": "GitHub のレート制限に達しました。数分後にもう一度お試しください。", + "issuePermissionDenied": "この Issue を編集する権限がありません。GitHub トークンのスコープを確認してください。", + "issuesDisabled": "このリポジトリでは Issue が無効になっています。", + "pullRequestPermissionDenied": "この PR を編集する権限がありません。GitHub トークンのスコープを確認してください。", + "pullRequestNotFound": "PR が見つかりません — 削除された可能性があります。", + "pullRequestUpdateFailed": "PR を更新できませんでした: {{detail}}", + "autoMergeEnableFailed": "PR の自動マージを有効にできませんでした: {{detail}}", + "autoMergeDisableFailed": "PR の自動マージを無効にできませんでした: {{detail}}", + "mergeQueueAddFailed": "PR をマージキューに追加できませんでした: {{detail}}", + "rerunPermissionDenied": "このリポジトリでチェックを再実行する権限がありません。GitHub トークンのスコープを確認してください。", + "rerunTargetNotFound": "再実行対象の GitHub リソースが見つかりません — 期限切れまたは削除された可能性があります。", + "rerunRejected": "チェックを再実行できませんでした — {{detail}}", + "rerunFailed": "チェックを再実行できませんでした: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "このリポジトリの GitLab プロジェクトを特定できませんでした", + "unexpectedResponse": "GitLab から予期しない応答がありました", + "commentBodyRequired": "コメント本文が必要です", + "discussionIdRequired": "ディスカッション ID が必要です", + "readyTitleConflict": "MR をレビュー準備完了にする際にタイトルは更新できません", + "mergeRequestTitleUnreadable": "現在の MR のタイトルを読み取れませんでした", + "rateLimited": "GitLab のレート制限に達しました。数分後にもう一度お試しください。", + "issuePermissionDenied": "この Issue を編集する権限がありません。GitLab トークンのスコープを確認してください。", + "jobLogPermissionDenied": "このジョブのログを読む権限がありません。GitLab トークンのスコープを確認してください。", + "jobProjectNotFound": "このジョブの GitLab プロジェクトが見つかりませんでした。", + "jobLogFailed": "ジョブログを読み込めませんでした: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 2456e63a1b1..e856f874b83 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -21805,5 +21805,53 @@ "unsupported": "선택한 자동화 호스트가 이 프로젝트를 지원하지 않습니다." } } + }, + "codeHost": { + "errors": { + "titleRequired": "제목은 필수 항목입니다", + "networkError": "네트워크 오류 — 연결을 확인하세요.", + "invalidUpdate": "잘못된 업데이트 — {{detail}}", + "invalidRequest": "잘못된 요청 — {{detail}}", + "issueNotFound": "이슈를 찾을 수 없습니다 — 삭제되었을 수 있습니다.", + "issueUpdateFailed": "이슈를 업데이트하지 못했습니다: {{detail}}", + "github": { + "repoUnresolved": "이 리포지토리의 GitHub 소유자와 리포지토리 이름을 확인할 수 없습니다", + "unexpectedResponse": "GitHub에서 예상치 못한 응답을 받았습니다", + "pullRequestIdUnresolved": "GitHub PR ID를 확인할 수 없습니다", + "reviewerRequired": "리뷰어를 한 명 이상 입력하세요", + "stackAutoMergeUnsupported": "GitHub는 스택에 포함된 PR의 자동 병합을 지원하지 않습니다.", + "autoMergeAlreadyMergeable": "이 PR은 이미 병합할 수 있습니다. 자동 병합 대신 '병합'을 사용하세요.", + "noFailedChecks": "다시 실행할 실패한 GitHub Actions 체크가 없습니다.", + "noRerunnableChecks": "다시 실행할 수 있는 체크를 찾을 수 없습니다.", + "stackMergeInvalidResponse": "GitHub가 잘못된 스택 병합 응답을 반환했습니다.", + "stackMergePending": "GitHub에서 아직 이 스택을 병합 중입니다. 새로고침하여 상태를 확인하세요.", + "rateLimited": "GitHub 속도 제한에 도달했습니다. 몇 분 후 다시 시도하세요.", + "issuePermissionDenied": "이 이슈를 편집할 권한이 없습니다. GitHub 토큰 범위를 확인하세요.", + "issuesDisabled": "이 리포지토리에서는 이슈가 비활성화되어 있습니다.", + "pullRequestPermissionDenied": "이 PR을 편집할 권한이 없습니다. GitHub 토큰 범위를 확인하세요.", + "pullRequestNotFound": "PR을 찾을 수 없습니다 — 삭제되었을 수 있습니다.", + "pullRequestUpdateFailed": "PR을 업데이트하지 못했습니다: {{detail}}", + "autoMergeEnableFailed": "PR 자동 병합을 활성화하지 못했습니다: {{detail}}", + "autoMergeDisableFailed": "PR 자동 병합을 비활성화하지 못했습니다: {{detail}}", + "mergeQueueAddFailed": "PR을 병합 대기열에 추가하지 못했습니다: {{detail}}", + "rerunPermissionDenied": "이 리포지토리에서 체크를 다시 실행할 권한이 없습니다. GitHub 토큰 범위를 확인하세요.", + "rerunTargetNotFound": "다시 실행할 GitHub 리소스를 찾을 수 없습니다 — 만료되었거나 삭제되었을 수 있습니다.", + "rerunRejected": "체크를 다시 실행하지 못했습니다 — {{detail}}", + "rerunFailed": "체크를 다시 실행하지 못했습니다: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "이 리포지토리의 GitLab 프로젝트를 확인할 수 없습니다", + "unexpectedResponse": "GitLab에서 예상치 못한 응답을 받았습니다", + "commentBodyRequired": "댓글 내용이 필요합니다", + "discussionIdRequired": "토론 ID가 필요합니다", + "readyTitleConflict": "MR을 검토 준비 완료로 표시하는 동안 제목을 업데이트할 수 없습니다", + "mergeRequestTitleUnreadable": "현재 MR 제목을 읽을 수 없습니다", + "rateLimited": "GitLab 속도 제한에 도달했습니다. 몇 분 후 다시 시도하세요.", + "issuePermissionDenied": "이 이슈를 편집할 권한이 없습니다. GitLab 토큰 범위를 확인하세요.", + "jobLogPermissionDenied": "이 작업의 로그를 읽을 권한이 없습니다. GitLab 토큰 범위를 확인하세요.", + "jobProjectNotFound": "이 작업의 GitLab 프로젝트를 찾을 수 없습니다.", + "jobLogFailed": "작업 로그를 불러오지 못했습니다: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json index ce816a5cfe2..c208139e1ea 100644 --- a/src/renderer/src/i18n/locales/pt.json +++ b/src/renderer/src/i18n/locales/pt.json @@ -22234,5 +22234,53 @@ "unsupported": "O host selecionado para a automação não oferece suporte a este projeto." } } + }, + "codeHost": { + "errors": { + "titleRequired": "O título é obrigatório", + "networkError": "Erro de rede — verifique sua conexão.", + "invalidUpdate": "Atualização inválida — {{detail}}", + "invalidRequest": "Solicitação inválida — {{detail}}", + "issueNotFound": "Issue não encontrada — ela pode ter sido excluída.", + "issueUpdateFailed": "Falha ao atualizar a issue: {{detail}}", + "github": { + "repoUnresolved": "Não foi possível determinar o owner/repo do GitHub para este repositório", + "unexpectedResponse": "Resposta inesperada do GitHub", + "pullRequestIdUnresolved": "Não foi possível determinar o ID do pull request no GitHub", + "reviewerRequired": "Informe pelo menos um revisor", + "stackAutoMergeUnsupported": "O GitHub não oferece suporte a merge automático de pull requests empilhados.", + "autoMergeAlreadyMergeable": "Este pull request já pode ser mergeado. Use Merge em vez de merge automático.", + "noFailedChecks": "Não há verificações com falha do GitHub Actions para executar novamente.", + "noRerunnableChecks": "Não foram encontradas verificações para executar novamente.", + "stackMergeInvalidResponse": "O GitHub retornou uma resposta inválida ao fazer merge da pilha.", + "stackMergePending": "O GitHub ainda está fazendo merge desta pilha. Atualize para verificar o status.", + "rateLimited": "Limite de taxa do GitHub atingido. Tente novamente em alguns minutos.", + "issuePermissionDenied": "Você não tem permissão para editar esta issue. Verifique os escopos do seu token do GitHub.", + "issuesDisabled": "As issues estão desativadas neste repositório.", + "pullRequestPermissionDenied": "Você não tem permissão para editar este pull request. Verifique os escopos do seu token do GitHub.", + "pullRequestNotFound": "Pull request não encontrado — ele pode ter sido excluído.", + "pullRequestUpdateFailed": "Falha ao atualizar o pull request: {{detail}}", + "autoMergeEnableFailed": "Falha ao ativar o merge automático do pull request: {{detail}}", + "autoMergeDisableFailed": "Falha ao desativar o merge automático do pull request: {{detail}}", + "mergeQueueAddFailed": "Falha ao adicionar o pull request à fila de merge: {{detail}}", + "rerunPermissionDenied": "Você não tem permissão para executar verificações novamente neste repositório. Verifique os escopos do seu token do GitHub.", + "rerunTargetNotFound": "O recurso do GitHub a ser executado novamente não foi encontrado — ele pode ter expirado ou sido excluído.", + "rerunRejected": "Não foi possível executar as verificações novamente — {{detail}}", + "rerunFailed": "Falha ao executar as verificações novamente: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "Não foi possível determinar o projeto do GitLab para este repositório", + "unexpectedResponse": "Resposta inesperada do GitLab", + "commentBodyRequired": "O conteúdo do comentário é obrigatório", + "discussionIdRequired": "O ID da discussão é obrigatório", + "readyTitleConflict": "Não é possível atualizar o título ao marcar um merge request como pronto", + "mergeRequestTitleUnreadable": "Não foi possível ler o título atual do merge request", + "rateLimited": "Limite de taxa do GitLab atingido. Tente novamente em alguns minutos.", + "issuePermissionDenied": "Você não tem permissão para editar esta issue. Verifique os escopos do seu token do GitLab.", + "jobLogPermissionDenied": "Você não tem permissão para ler o log deste job. Verifique os escopos do seu token do GitLab.", + "jobProjectNotFound": "Não foi possível encontrar o projeto do GitLab deste job.", + "jobLogFailed": "Falha ao carregar o log do job: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/ru.json b/src/renderer/src/i18n/locales/ru.json index d3b1cd2fed0..74c6cb05ae8 100644 --- a/src/renderer/src/i18n/locales/ru.json +++ b/src/renderer/src/i18n/locales/ru.json @@ -22475,5 +22475,53 @@ "unsupported": "Выбранный сервер автоматизации не поддерживает этот проект." } } + }, + "codeHost": { + "errors": { + "titleRequired": "Укажите название", + "networkError": "Ошибка сети — проверьте подключение.", + "invalidUpdate": "Некорректное обновление — {{detail}}", + "invalidRequest": "Некорректный запрос — {{detail}}", + "issueNotFound": "Задача не найдена — возможно, она была удалена.", + "issueUpdateFailed": "Не удалось обновить задачу: {{detail}}", + "github": { + "repoUnresolved": "Не удалось определить owner/repo на GitHub для этого репозитория", + "unexpectedResponse": "Неожиданный ответ от GitHub", + "pullRequestIdUnresolved": "Не удалось определить ID пул-реквеста GitHub", + "reviewerRequired": "Укажите хотя бы одного рецензента", + "stackAutoMergeUnsupported": "GitHub не поддерживает автослияние пул-реквестов в стеке.", + "autoMergeAlreadyMergeable": "Этот пул-реквест уже можно слить. Используйте «Слить» вместо автослияния.", + "noFailedChecks": "Нет неуспешных проверок GitHub Actions для повторного запуска.", + "noRerunnableChecks": "Проверки для повторного запуска не найдены.", + "stackMergeInvalidResponse": "GitHub вернул некорректный ответ на слияние стека.", + "stackMergePending": "GitHub ещё выполняет слияние этого стека. Обновите, чтобы проверить статус.", + "rateLimited": "Достигнут лимит запросов GitHub. Повторите попытку через несколько минут.", + "issuePermissionDenied": "У вас нет разрешения на редактирование этой задачи. Проверьте права токена GitHub.", + "issuesDisabled": "Задачи в этом репозитории отключены.", + "pullRequestPermissionDenied": "У вас нет разрешения на редактирование этого пул-реквеста. Проверьте права токена GitHub.", + "pullRequestNotFound": "Пул-реквест не найден — возможно, он был удалён.", + "pullRequestUpdateFailed": "Не удалось обновить пул-реквест: {{detail}}", + "autoMergeEnableFailed": "Не удалось включить автослияние пул-реквеста: {{detail}}", + "autoMergeDisableFailed": "Не удалось отключить автослияние пул-реквеста: {{detail}}", + "mergeQueueAddFailed": "Не удалось добавить пул-реквест в очередь слияния: {{detail}}", + "rerunPermissionDenied": "У вас нет разрешения на повторный запуск проверок в этом репозитории. Проверьте права токена GitHub.", + "rerunTargetNotFound": "Ресурс GitHub для повторного запуска не найден — возможно, срок его действия истёк или он был удалён.", + "rerunRejected": "Не удалось повторно запустить проверки — {{detail}}", + "rerunFailed": "Не удалось повторно запустить проверки: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "Не удалось определить проект GitLab для этого репозитория", + "unexpectedResponse": "Неожиданный ответ от GitLab", + "commentBodyRequired": "Необходимо ввести текст комментария", + "discussionIdRequired": "Требуется ID обсуждения", + "readyTitleConflict": "Нельзя обновить название и одновременно отметить запрос на слияние как готовый", + "mergeRequestTitleUnreadable": "Не удалось прочитать текущее название запроса на слияние", + "rateLimited": "Достигнут лимит запросов GitLab. Повторите попытку через несколько минут.", + "issuePermissionDenied": "У вас нет разрешения на редактирование этой задачи. Проверьте права токена GitLab.", + "jobLogPermissionDenied": "У вас нет разрешения на чтение журнала этого задания. Проверьте права токена GitLab.", + "jobProjectNotFound": "Не удалось найти проект GitLab для этого задания.", + "jobLogFailed": "Не удалось загрузить журнал задания: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/tr.json b/src/renderer/src/i18n/locales/tr.json index b522a884977..b6a9242b42d 100644 --- a/src/renderer/src/i18n/locales/tr.json +++ b/src/renderer/src/i18n/locales/tr.json @@ -21783,5 +21783,53 @@ "unsupported": "Seçili otomasyon sunucusu bu projeyi desteklemiyor." } } + }, + "codeHost": { + "errors": { + "titleRequired": "Başlık zorunludur", + "networkError": "Ağ hatası — bağlantınızı kontrol edin.", + "invalidUpdate": "Geçersiz güncelleme — {{detail}}", + "invalidRequest": "Geçersiz istek — {{detail}}", + "issueNotFound": "Sorun bulunamadı — silinmiş olabilir.", + "issueUpdateFailed": "Sorun güncellenemedi: {{detail}}", + "github": { + "repoUnresolved": "Bu repository için GitHub owner/repo bilgisi belirlenemedi", + "unexpectedResponse": "GitHub'dan beklenmeyen yanıt", + "pullRequestIdUnresolved": "GitHub pull request ID'si belirlenemedi", + "reviewerRequired": "En az bir inceleyen belirtin", + "stackAutoMergeUnsupported": "GitHub, yığındaki pull request'ler için otomatik merge'i desteklemiyor.", + "autoMergeAlreadyMergeable": "Bu pull request zaten merge edilebilir. Otomatik merge yerine Merge kullanın.", + "noFailedChecks": "Yeniden çalıştırılacak başarısız GitHub Actions kontrolü yok.", + "noRerunnableChecks": "Yeniden çalıştırılabilir kontrol bulunamadı.", + "stackMergeInvalidResponse": "GitHub geçersiz bir yığın merge yanıtı döndürdü.", + "stackMergePending": "GitHub hâlâ bu yığını merge ediyor. Durumunu kontrol etmek için yenileyin.", + "rateLimited": "GitHub istek sınırına ulaşıldı. Birkaç dakika sonra tekrar deneyin.", + "issuePermissionDenied": "Bu sorunu düzenleme izniniz yok. GitHub belirtecinizin kapsamlarını kontrol edin.", + "issuesDisabled": "Bu repository'de sorunlar devre dışı.", + "pullRequestPermissionDenied": "Bu pull request'i düzenleme izniniz yok. GitHub belirtecinizin kapsamlarını kontrol edin.", + "pullRequestNotFound": "Pull request bulunamadı — silinmiş olabilir.", + "pullRequestUpdateFailed": "Pull request güncellenemedi: {{detail}}", + "autoMergeEnableFailed": "Pull request için otomatik merge etkinleştirilemedi: {{detail}}", + "autoMergeDisableFailed": "Pull request için otomatik merge devre dışı bırakılamadı: {{detail}}", + "mergeQueueAddFailed": "Pull request merge queue'suna eklenemedi: {{detail}}", + "rerunPermissionDenied": "Bu repository'de kontrolleri yeniden çalıştırma izniniz yok. GitHub belirtecinizin kapsamlarını kontrol edin.", + "rerunTargetNotFound": "Yeniden çalıştırılacak GitHub kaynağı bulunamadı — süresi dolmuş veya silinmiş olabilir.", + "rerunRejected": "Kontroller yeniden çalıştırılamadı — {{detail}}", + "rerunFailed": "Kontroller yeniden çalıştırılamadı: {{detail}}" + }, + "gitlab": { + "projectUnresolved": "Bu repository için GitLab projesi belirlenemedi", + "unexpectedResponse": "GitLab'dan beklenmeyen yanıt", + "commentBodyRequired": "Yorum metni gerekli", + "discussionIdRequired": "Tartışma ID'si gerekli", + "readyTitleConflict": "Bir merge request hazır olarak işaretlenirken başlık güncellenemez", + "mergeRequestTitleUnreadable": "Merge request'in mevcut başlığı okunamadı", + "rateLimited": "GitLab istek sınırına ulaşıldı. Birkaç dakika sonra tekrar deneyin.", + "issuePermissionDenied": "Bu sorunu düzenleme izniniz yok. GitLab belirtecinizin kapsamlarını kontrol edin.", + "jobLogPermissionDenied": "Bu işin günlüğünü okuma izniniz yok. GitLab belirtecinizin kapsamlarını kontrol edin.", + "jobProjectNotFound": "Bu işin GitLab projesi bulunamadı.", + "jobLogFailed": "İş günlüğü yüklenemedi: {{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/zh-Hant.json b/src/renderer/src/i18n/locales/zh-Hant.json index 438ab10ddfd..53a6e7615a1 100644 --- a/src/renderer/src/i18n/locales/zh-Hant.json +++ b/src/renderer/src/i18n/locales/zh-Hant.json @@ -21505,5 +21505,53 @@ "unsupported": "所選自動化主機不支援此專案。" } } + }, + "codeHost": { + "errors": { + "titleRequired": "標題為必填欄位", + "networkError": "網路錯誤,請檢查連線。", + "invalidUpdate": "無效的更新 — {{detail}}", + "invalidRequest": "無效的請求 — {{detail}}", + "issueNotFound": "找不到議題 — 可能已遭刪除。", + "issueUpdateFailed": "無法更新議題:{{detail}}", + "github": { + "repoUnresolved": "無法確認此存放庫的 GitHub 擁有者和存放庫名稱", + "unexpectedResponse": "GitHub 傳回非預期的回應", + "pullRequestIdUnresolved": "無法確認 GitHub PR ID", + "reviewerRequired": "請至少輸入一位審查人", + "stackAutoMergeUnsupported": "GitHub 不支援堆疊中 PR 的自動合併。", + "autoMergeAlreadyMergeable": "此 PR 已可合併。請使用「合併」而非自動合併。", + "noFailedChecks": "沒有失敗的 GitHub Actions 檢查可重新執行。", + "noRerunnableChecks": "找不到可重新執行的檢查。", + "stackMergeInvalidResponse": "GitHub 傳回無效的堆疊合併回應。", + "stackMergePending": "GitHub 仍在合併此堆疊。請重新整理以查看狀態。", + "rateLimited": "已達 GitHub 速率限制。請稍候幾分鐘再試。", + "issuePermissionDenied": "你沒有權限編輯此議題。請檢查 GitHub 權杖範圍。", + "issuesDisabled": "此存放庫已停用議題。", + "pullRequestPermissionDenied": "你沒有權限編輯此 PR。請檢查 GitHub 權杖範圍。", + "pullRequestNotFound": "找不到 PR — 可能已遭刪除。", + "pullRequestUpdateFailed": "無法更新 PR:{{detail}}", + "autoMergeEnableFailed": "無法為 PR 啟用自動合併:{{detail}}", + "autoMergeDisableFailed": "無法為 PR 停用自動合併:{{detail}}", + "mergeQueueAddFailed": "無法將 PR 新增至合併佇列:{{detail}}", + "rerunPermissionDenied": "你沒有權限在此存放庫重新執行檢查。請檢查 GitHub 權杖範圍。", + "rerunTargetNotFound": "找不到要重新執行的 GitHub 資源 — 可能已過期或遭刪除。", + "rerunRejected": "無法重新執行檢查 — {{detail}}", + "rerunFailed": "無法重新執行檢查:{{detail}}" + }, + "gitlab": { + "projectUnresolved": "無法確認此存放庫的 GitLab 專案", + "unexpectedResponse": "GitLab 傳回非預期的回應", + "commentBodyRequired": "必須填寫留言內容", + "discussionIdRequired": "必須提供討論 ID", + "readyTitleConflict": "將合併請求標記為可供審查時無法更新標題", + "mergeRequestTitleUnreadable": "無法讀取合併請求的目前標題", + "rateLimited": "已達 GitLab 速率限制。請稍候幾分鐘再試。", + "issuePermissionDenied": "你沒有權限編輯此議題。請檢查 GitLab 權杖範圍。", + "jobLogPermissionDenied": "你沒有權限讀取此作業的日誌。請檢查 GitLab 權杖範圍。", + "jobProjectNotFound": "找不到此作業的 GitLab 專案。", + "jobLogFailed": "無法載入作業日誌:{{detail}}" + } + } } } diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 486d03cd3dd..c7c13c34342 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -21805,5 +21805,53 @@ "unsupported": "所选自动化主机不支持此项目。" } } + }, + "codeHost": { + "errors": { + "titleRequired": "标题为必填项", + "networkError": "网络错误 — 请检查你的网络连接。", + "invalidUpdate": "无效的更新 — {{detail}}", + "invalidRequest": "无效的请求 — {{detail}}", + "issueNotFound": "未找到议题 — 可能已被删除。", + "issueUpdateFailed": "更新议题失败:{{detail}}", + "github": { + "repoUnresolved": "无法确定此仓库的 GitHub 所有者和仓库名称", + "unexpectedResponse": "GitHub 返回了意外响应", + "pullRequestIdUnresolved": "无法确定 GitHub PR ID", + "reviewerRequired": "请至少输入一位评审人", + "stackAutoMergeUnsupported": "GitHub 不支持堆栈中 PR 的自动合并。", + "autoMergeAlreadyMergeable": "此 PR 已可合并。请使用“合并”而非自动合并。", + "noFailedChecks": "没有失败的 GitHub Actions 检查可重新运行。", + "noRerunnableChecks": "未找到可重新运行的检查。", + "stackMergeInvalidResponse": "GitHub 返回了无效的堆栈合并响应。", + "stackMergePending": "GitHub 仍在合并此堆栈。请刷新以查看状态。", + "rateLimited": "已达到 GitHub 速率限制。请几分钟后重试。", + "issuePermissionDenied": "你没有权限编辑此议题。请检查 GitHub 令牌范围。", + "issuesDisabled": "此仓库已禁用议题。", + "pullRequestPermissionDenied": "你没有权限编辑此 PR。请检查 GitHub 令牌范围。", + "pullRequestNotFound": "未找到 PR — 可能已被删除。", + "pullRequestUpdateFailed": "更新 PR 失败:{{detail}}", + "autoMergeEnableFailed": "为 PR 启用自动合并失败:{{detail}}", + "autoMergeDisableFailed": "为 PR 禁用自动合并失败:{{detail}}", + "mergeQueueAddFailed": "将 PR 添加到合并队列失败:{{detail}}", + "rerunPermissionDenied": "你没有权限在此仓库中重新运行检查。请检查 GitHub 令牌范围。", + "rerunTargetNotFound": "未找到要重新运行的 GitHub 资源 — 可能已过期或被删除。", + "rerunRejected": "无法重新运行检查 — {{detail}}", + "rerunFailed": "重新运行检查失败:{{detail}}" + }, + "gitlab": { + "projectUnresolved": "无法确定此仓库的 GitLab 项目", + "unexpectedResponse": "GitLab 返回了意外响应", + "commentBodyRequired": "必须填写评论正文", + "discussionIdRequired": "必须提供讨论 ID", + "readyTitleConflict": "将合并请求标记为就绪时无法更新标题", + "mergeRequestTitleUnreadable": "无法读取合并请求的当前标题", + "rateLimited": "已达到 GitLab 速率限制。请几分钟后重试。", + "issuePermissionDenied": "你没有权限编辑此议题。请检查 GitLab 令牌范围。", + "jobLogPermissionDenied": "你没有权限读取此作业的日志。请检查 GitLab 令牌范围。", + "jobProjectNotFound": "找不到此作业的 GitLab 项目。", + "jobLogFailed": "加载作业日志失败:{{detail}}" + } + } } } diff --git a/src/renderer/src/lib/code-host-error-copy.test.ts b/src/renderer/src/lib/code-host-error-copy.test.ts new file mode 100644 index 00000000000..c6d93b91b17 --- /dev/null +++ b/src/renderer/src/lib/code-host-error-copy.test.ts @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { i18n } from '@/i18n/i18n' +import es from '@/i18n/locales/es.json' +import { codeHostFailure } from '../../../shared/code-host-failure-copy' +import { codeHostErrorText } from './code-host-error-copy' + +const originalLanguage = i18n.language + +afterEach(async () => { + await i18n.changeLanguage(originalLanguage) +}) + +describe('codeHostErrorText', () => { + it('localizes a coded failure and keeps the gh/glab detail verbatim', async () => { + i18n.addResourceBundle('es', 'translation', es) + await i18n.changeLanguage('es') + const unresolved = codeHostFailure('github.repoUnresolved') + expect(unresolved.error).toBe('Could not resolve GitHub owner/repo for this repository') + expect(codeHostErrorText(unresolved)).toBe(es.codeHost.errors.github.repoUnresolved) + + const detail = 'HTTP 422: {{detail}} $& rejected' + const failed = codeHostFailure('github.rerunFailed', detail) + expect(failed.error).toBe(`Failed to rerun checks: ${detail}`) + expect(codeHostErrorText(failed)).toBe( + es.codeHost.errors.github.rerunFailed.replace('{{detail}}', () => detail) + ) + }) + + it('shows the host English for uncoded, unknown, or incomplete failures', async () => { + i18n.addResourceBundle('es', 'translation', es) + await i18n.changeLanguage('es') + expect(codeHostErrorText({ error: 'Old host text' })).toBe('Old host text') + expect(codeHostErrorText({ error: 'Newer host text', errorCode: 'github.future' })).toBe( + 'Newer host text' + ) + expect(codeHostErrorText({ error: 'English', errorCode: 'github.rerunFailed' })).toBe('English') + expect(codeHostErrorText({ error: 'English', errorCode: 'toString' })).toBe('English') + }) +}) diff --git a/src/renderer/src/lib/code-host-error-copy.ts b/src/renderer/src/lib/code-host-error-copy.ts new file mode 100644 index 00000000000..71f767b7685 --- /dev/null +++ b/src/renderer/src/lib/code-host-error-copy.ts @@ -0,0 +1,159 @@ +import { translate } from '@/i18n/i18n' +import { + CODE_HOST_FAILURE_COPY as EN, + type CodeHostFailureCode +} from '../../../shared/code-host-failure-copy' + +// null = required detail missing; caller shows the host's English `error`. +const COPY: Record string | null> = { + titleRequired: () => translate('codeHost.errors.titleRequired', EN.titleRequired), + networkError: () => translate('codeHost.errors.networkError', EN.networkError), + invalidUpdate: (detail) => + detail === null + ? null + : translate('codeHost.errors.invalidUpdate', EN.invalidUpdate, { detail }), + invalidRequest: (detail) => + detail === null + ? null + : translate('codeHost.errors.invalidRequest', EN.invalidRequest, { detail }), + issueNotFound: () => translate('codeHost.errors.issueNotFound', EN.issueNotFound), + issueUpdateFailed: (detail) => + detail === null + ? null + : translate('codeHost.errors.issueUpdateFailed', EN.issueUpdateFailed, { detail }), + 'github.repoUnresolved': () => + translate('codeHost.errors.github.repoUnresolved', EN['github.repoUnresolved']), + 'github.unexpectedResponse': () => + translate('codeHost.errors.github.unexpectedResponse', EN['github.unexpectedResponse']), + 'github.pullRequestIdUnresolved': () => + translate( + 'codeHost.errors.github.pullRequestIdUnresolved', + EN['github.pullRequestIdUnresolved'] + ), + 'github.reviewerRequired': () => + translate('codeHost.errors.github.reviewerRequired', EN['github.reviewerRequired']), + 'github.stackAutoMergeUnsupported': () => + translate( + 'codeHost.errors.github.stackAutoMergeUnsupported', + EN['github.stackAutoMergeUnsupported'] + ), + 'github.autoMergeAlreadyMergeable': () => + translate( + 'codeHost.errors.github.autoMergeAlreadyMergeable', + EN['github.autoMergeAlreadyMergeable'] + ), + 'github.noFailedChecks': () => + translate('codeHost.errors.github.noFailedChecks', EN['github.noFailedChecks']), + 'github.noRerunnableChecks': () => + translate('codeHost.errors.github.noRerunnableChecks', EN['github.noRerunnableChecks']), + 'github.stackMergeInvalidResponse': () => + translate( + 'codeHost.errors.github.stackMergeInvalidResponse', + EN['github.stackMergeInvalidResponse'] + ), + 'github.stackMergePending': () => + translate('codeHost.errors.github.stackMergePending', EN['github.stackMergePending']), + 'github.rateLimited': () => + translate('codeHost.errors.github.rateLimited', EN['github.rateLimited']), + 'github.issuePermissionDenied': () => + translate('codeHost.errors.github.issuePermissionDenied', EN['github.issuePermissionDenied']), + 'github.issuesDisabled': () => + translate('codeHost.errors.github.issuesDisabled', EN['github.issuesDisabled']), + 'github.pullRequestPermissionDenied': () => + translate( + 'codeHost.errors.github.pullRequestPermissionDenied', + EN['github.pullRequestPermissionDenied'] + ), + 'github.pullRequestNotFound': () => + translate('codeHost.errors.github.pullRequestNotFound', EN['github.pullRequestNotFound']), + 'github.pullRequestUpdateFailed': (detail) => + detail === null + ? null + : translate( + 'codeHost.errors.github.pullRequestUpdateFailed', + EN['github.pullRequestUpdateFailed'], + { detail } + ), + 'github.autoMergeEnableFailed': (detail) => + detail === null + ? null + : translate( + 'codeHost.errors.github.autoMergeEnableFailed', + EN['github.autoMergeEnableFailed'], + { detail } + ), + 'github.autoMergeDisableFailed': (detail) => + detail === null + ? null + : translate( + 'codeHost.errors.github.autoMergeDisableFailed', + EN['github.autoMergeDisableFailed'], + { detail } + ), + 'github.mergeQueueAddFailed': (detail) => + detail === null + ? null + : translate('codeHost.errors.github.mergeQueueAddFailed', EN['github.mergeQueueAddFailed'], { + detail + }), + 'github.rerunPermissionDenied': () => + translate('codeHost.errors.github.rerunPermissionDenied', EN['github.rerunPermissionDenied']), + 'github.rerunTargetNotFound': () => + translate('codeHost.errors.github.rerunTargetNotFound', EN['github.rerunTargetNotFound']), + 'github.rerunRejected': (detail) => + detail === null + ? null + : translate('codeHost.errors.github.rerunRejected', EN['github.rerunRejected'], { detail }), + 'github.rerunFailed': (detail) => + detail === null + ? null + : translate('codeHost.errors.github.rerunFailed', EN['github.rerunFailed'], { detail }), + 'gitlab.projectUnresolved': () => + translate('codeHost.errors.gitlab.projectUnresolved', EN['gitlab.projectUnresolved']), + 'gitlab.unexpectedResponse': () => + translate('codeHost.errors.gitlab.unexpectedResponse', EN['gitlab.unexpectedResponse']), + 'gitlab.commentBodyRequired': () => + translate('codeHost.errors.gitlab.commentBodyRequired', EN['gitlab.commentBodyRequired']), + 'gitlab.discussionIdRequired': () => + translate('codeHost.errors.gitlab.discussionIdRequired', EN['gitlab.discussionIdRequired']), + 'gitlab.readyTitleConflict': () => + translate('codeHost.errors.gitlab.readyTitleConflict', EN['gitlab.readyTitleConflict']), + 'gitlab.mergeRequestTitleUnreadable': () => + translate( + 'codeHost.errors.gitlab.mergeRequestTitleUnreadable', + EN['gitlab.mergeRequestTitleUnreadable'] + ), + 'gitlab.rateLimited': () => + translate('codeHost.errors.gitlab.rateLimited', EN['gitlab.rateLimited']), + 'gitlab.issuePermissionDenied': () => + translate('codeHost.errors.gitlab.issuePermissionDenied', EN['gitlab.issuePermissionDenied']), + 'gitlab.jobLogPermissionDenied': () => + translate('codeHost.errors.gitlab.jobLogPermissionDenied', EN['gitlab.jobLogPermissionDenied']), + 'gitlab.jobProjectNotFound': () => + translate('codeHost.errors.gitlab.jobProjectNotFound', EN['gitlab.jobProjectNotFound']), + 'gitlab.jobLogFailed': (detail) => + detail === null + ? null + : translate('codeHost.errors.gitlab.jobLogFailed', EN['gitlab.jobLogFailed'], { detail }) +} + +function isCodeHostFailureCode(code: unknown): code is CodeHostFailureCode { + return typeof code === 'string' && Object.hasOwn(COPY, code) +} + +type CodeHostFailureLike = { errorCode?: unknown; errorDetail?: unknown } + +/** GitHub/GitLab operation failure text; uncoded (older host) or unknown (newer host) results show the English `error`. */ +export function codeHostErrorText(failure: CodeHostFailureLike & { error: string }): string +export function codeHostErrorText( + failure: CodeHostFailureLike & { error?: string } +): string | undefined +export function codeHostErrorText( + failure: CodeHostFailureLike & { error?: string } +): string | undefined { + if (!isCodeHostFailureCode(failure.errorCode)) { + return failure.error + } + const detail = typeof failure.errorDetail === 'string' ? failure.errorDetail : null + return COPY[failure.errorCode](detail) ?? failure.error +} diff --git a/src/renderer/src/runtime/gitlab-job-trace-client.ts b/src/renderer/src/runtime/gitlab-job-trace-client.ts index 9be45b29eb5..8f0e76617da 100644 --- a/src/renderer/src/runtime/gitlab-job-trace-client.ts +++ b/src/renderer/src/runtime/gitlab-job-trace-client.ts @@ -7,6 +7,7 @@ import type { GitLabProjectRef } from '../../../shared/gitlab-types' import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/github/check-types' import { gitLabApiFor, JOB_TRACE_TIMEOUT_MS } from './gitlab-owner-api' import { withGitLabIpcTimeout } from './gitlab-ipc-timeout' +import { codeHostErrorText } from '@/lib/code-host-error-copy' /** Load GitLab job logs as provider-neutral details; null permits other-provider fallback, while GitLab failures throw for caller error UI. */ export async function loadGitLabJobLogDetails(args: { @@ -35,7 +36,7 @@ export async function loadGitLabJobLogDetails(args: { ) if (!result?.ok) { throw new Error( - result?.error?.trim() || + (result ? codeHostErrorText(result) : undefined)?.trim() || translate( 'auto.runtime.gitlabJobTraceClient.loadFailed', 'Failed to load the GitLab job log.' diff --git a/src/shared/classified-error.ts b/src/shared/classified-error.ts index 68e7bcf96a9..b00a56ee9eb 100644 --- a/src/shared/classified-error.ts +++ b/src/shared/classified-error.ts @@ -8,4 +8,8 @@ export type ClassifiedError = { | 'network_error' | 'unknown' message: string + /** Copy code for `message`; older hosts omit it. */ + code?: string + /** Verbatim gh/glab text embedded in `message`. */ + detail?: string } diff --git a/src/shared/code-host-failure-copy.ts b/src/shared/code-host-failure-copy.ts new file mode 100644 index 00000000000..cf1343ecc2c --- /dev/null +++ b/src/shared/code-host-failure-copy.ts @@ -0,0 +1,106 @@ +import type { ClassifiedError } from './classified-error' + +// One English table for GitHub/GitLab operation failures: main builds the wire `error`, the renderer translates by code. +export const CODE_HOST_FAILURE_COPY = { + titleRequired: 'Title is required', + networkError: 'Network error — check your connection.', + invalidUpdate: 'Invalid update — {{detail}}', + invalidRequest: 'Invalid request — {{detail}}', + issueNotFound: 'Issue not found — it may have been deleted.', + issueUpdateFailed: 'Failed to update issue: {{detail}}', + 'github.repoUnresolved': 'Could not resolve GitHub owner/repo for this repository', + 'github.unexpectedResponse': 'Unexpected response from GitHub', + 'github.pullRequestIdUnresolved': 'Could not resolve GitHub pull request ID', + 'github.reviewerRequired': 'Enter at least one reviewer', + 'github.stackAutoMergeUnsupported': + 'GitHub does not support auto-merge for stacked pull requests.', + 'github.autoMergeAlreadyMergeable': + 'This pull request can already be merged. Use Merge instead of auto-merge.', + 'github.noFailedChecks': 'No failed GitHub Actions checks to rerun.', + 'github.noRerunnableChecks': 'No rerunnable checks found.', + 'github.stackMergeInvalidResponse': 'GitHub returned an invalid stack merge response.', + 'github.stackMergePending': 'GitHub is still merging this stack. Refresh to check its status.', + 'github.rateLimited': 'GitHub rate limit hit. Try again in a few minutes.', + 'github.issuePermissionDenied': + "You don't have permission to edit this issue. Check your GitHub token scopes.", + 'github.issuesDisabled': 'Issues are disabled on this repository.', + 'github.pullRequestPermissionDenied': + "You don't have permission to edit this pull request. Check your GitHub token scopes.", + 'github.pullRequestNotFound': 'Pull request not found — it may have been deleted.', + 'github.pullRequestUpdateFailed': 'Failed to update pull request: {{detail}}', + 'github.autoMergeEnableFailed': 'Failed to enable auto-merge for pull request: {{detail}}', + 'github.autoMergeDisableFailed': 'Failed to disable auto-merge for pull request: {{detail}}', + 'github.mergeQueueAddFailed': 'Failed to add pull request to the merge queue: {{detail}}', + 'github.rerunPermissionDenied': + "You don't have permission to rerun checks on this repository. Check your GitHub token scopes.", + 'github.rerunTargetNotFound': + 'GitHub resource to rerun was not found — it may have expired or been deleted.', + 'github.rerunRejected': 'Could not rerun checks — {{detail}}', + 'github.rerunFailed': 'Failed to rerun checks: {{detail}}', + 'gitlab.projectUnresolved': 'Could not resolve GitLab project for this repository', + 'gitlab.unexpectedResponse': 'Unexpected response from GitLab', + 'gitlab.commentBodyRequired': 'Comment body is required', + 'gitlab.discussionIdRequired': 'Discussion id is required', + 'gitlab.readyTitleConflict': 'Cannot update the title while marking a merge request ready', + 'gitlab.mergeRequestTitleUnreadable': 'Could not read the current merge request title', + 'gitlab.rateLimited': 'GitLab rate limit hit. Try again in a few minutes.', + 'gitlab.issuePermissionDenied': + "You don't have permission to edit this issue. Check your GitLab token scopes.", + 'gitlab.jobLogPermissionDenied': + "You don't have permission to read this job's log. Check your GitLab token scopes.", + 'gitlab.jobProjectNotFound': "Could not find this job's GitLab project.", + 'gitlab.jobLogFailed': 'Failed to load the job log: {{detail}}' +} as const + +export type CodeHostFailureCode = keyof typeof CODE_HOST_FAILURE_COPY + +/** Wire shape: `error` stays English for older clients; `errorDetail` is verbatim gh/glab text. */ +export type CodeHostFailure = { + ok: false + error: string + errorCode?: string + errorDetail?: string +} + +export function codeHostFailureText(code: CodeHostFailureCode, detail?: string): string { + return CODE_HOST_FAILURE_COPY[code].replace('{{detail}}', () => detail ?? '') +} + +export function codeHostFailure(code: CodeHostFailureCode, detail?: string): CodeHostFailure { + return { + ok: false, + error: codeHostFailureText(code, detail), + errorCode: code, + ...(detail !== undefined ? { errorDetail: detail } : {}) + } +} + +export function classifiedCodeHostError( + type: ClassifiedError['type'], + code: CodeHostFailureCode, + detail?: string +): ClassifiedError { + return { + type, + message: codeHostFailureText(code, detail), + code, + ...(detail !== undefined ? { detail } : {}) + } +} + +/** Carries a classified gh/glab failure onto an `{ ok: false }` result. */ +export function classifiedFailure(classified: ClassifiedError): CodeHostFailure { + return { + ok: false, + error: classified.message, + ...(classified.code !== undefined ? { errorCode: classified.code } : {}), + ...(classified.detail !== undefined ? { errorDetail: classified.detail } : {}) + } +} + +/** A single failure keeps its code; several are joined in English only. */ +export function combinedClassifiedFailure(errors: readonly ClassifiedError[]): CodeHostFailure { + return errors.length === 1 + ? classifiedFailure(errors[0]) + : { ok: false, error: errors.map((e) => e.message).join('; ') } +}