feat: add external review link to source control branch panel

Add a button to the branch compare header that opens the review or
comparison page directly in the browser. Supports generating comparison
and PR/MR creation URLs for GitHub, GitLab, Bitbucket, Azure DevOps,
and Gitea.
This commit is contained in:
Jinjing
2026-07-05 16:14:28 -07:00
parent d6578af833
commit 8cbb92842e
11 changed files with 604 additions and 9 deletions
@@ -278,6 +278,7 @@ import {
resolveHostedReviewActionUpstreamStatus,
resolveHostedReviewStateForActions
} from './source-control-hosted-review-push-target'
import { buildSourceControlManualReviewUrl } from './source-control-manual-review-url'
export { HostedReviewHeaderLink } from './hosted-review-header-chrome'
import {
createRunningCommitMessageGenerationRecord,
@@ -1467,6 +1468,41 @@ function SourceControlInner(): React.JSX.Element {
const linkedBitbucketPR = activeWorktree?.linkedBitbucketPR ?? null
const linkedAzureDevOpsPR = activeWorktree?.linkedAzureDevOpsPR ?? null
const linkedGiteaPR = activeWorktree?.linkedGiteaPR ?? null
const manualReviewProvider: HostedReviewProvider | null =
hostedReview?.provider ??
hostedReviewCreation?.provider ??
(linkedGitLabMR !== null
? 'gitlab'
: linkedBitbucketPR !== null
? 'bitbucket'
: linkedAzureDevOpsPR !== null
? 'azure-devops'
: linkedGiteaPR !== null
? 'gitea'
: linkedGitHubPR !== null || fallbackGitHubPRNumber !== null
? 'github'
: null)
const manualReviewUrl = useMemo(
() =>
buildSourceControlManualReviewUrl({
baseRef: compareBaseRef,
branchName,
repoRemoteName: activeRepo?.gitRemoteIdentity?.remoteName ?? null,
repoRemoteUrl: activeRepo?.gitRemoteIdentity?.remoteUrl ?? null,
provider: manualReviewProvider,
pushTarget: activeWorktree?.pushTarget ?? null,
upstreamName: remoteStatus?.upstreamName ?? null
}),
[
activeRepo?.gitRemoteIdentity?.remoteName,
activeRepo?.gitRemoteIdentity?.remoteUrl,
activeWorktree?.pushTarget,
branchName,
compareBaseRef,
manualReviewProvider,
remoteStatus?.upstreamName
]
)
const shouldResolveHostedReviewCreation =
isBranchVisible &&
Boolean(activeRepo) &&
@@ -5415,6 +5451,7 @@ function SourceControlInner(): React.JSX.Element {
branchSummary={branchSummary}
compareBaseRef={compareBaseRef}
upstreamStatus={remoteStatus}
manualReviewUrl={manualReviewUrl}
/>
{detachedHeadDisplay && (
@@ -37,4 +37,18 @@ describe('SourceControlBranchContextRow', () => {
expect(markup).toContain('min-w-0 flex-1')
expect(markup).not.toContain('max-w-[9rem]')
})
it('renders a compact external review link when a manual URL is available', () => {
const markup = renderToStaticMarkup(
<SourceControlBranchContextRow
summary={readySummary}
compareBaseRef={null}
manualReviewUrl="https://github.com/stablyai/orca/compare/main...feature?expand=1"
onChangeBaseRef={vi.fn()}
onRetry={vi.fn()}
/>
)
expect(markup).toContain('aria-label="Open review page in browser"')
})
})
@@ -1,5 +1,5 @@
import React from 'react'
import { Loader2, RefreshCw } from 'lucide-react'
import { ExternalLink, Loader2, RefreshCw } from 'lucide-react'
import type { GitBranchCompareSummary, GitUpstreamStatus } from '../../../../shared/types'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
@@ -60,16 +60,40 @@ function ContextStat({
)
}
function ManualReviewLinkButton({
url
}: {
url: string | null | undefined
}): React.JSX.Element | null {
if (!url) {
return null
}
return (
<SourceControlHeaderIconButton
icon={ExternalLink}
label={translate(
'auto.components.right.sidebar.SourceControl.4b4a7de138',
'Open review page in browser'
)}
onClick={() => {
void window.api.shell.openUrl(url)
}}
/>
)
}
export function SourceControlBranchContextRow({
summary,
compareBaseRef,
upstreamStatus,
manualReviewUrl,
onChangeBaseRef,
onRetry
}: {
summary: GitBranchCompareSummary | null
compareBaseRef: string | null
upstreamStatus?: GitUpstreamStatus
manualReviewUrl?: string | null
onChangeBaseRef: () => void
onRetry: () => void
}): React.JSX.Element | null {
@@ -97,6 +121,7 @@ export function SourceControlBranchContextRow({
title={changeBaseTitle}
/>
</span>
<ManualReviewLinkButton url={manualReviewUrl} />
</div>
)
}
@@ -118,6 +143,7 @@ export function SourceControlBranchContextRow({
'Branch compare unavailable'
)}
</span>
<ManualReviewLinkButton url={manualReviewUrl} />
<SourceControlHeaderIconButton
icon={RefreshCw}
label={translate('auto.components.right.sidebar.SourceControl.286dbda4d6', 'Retry')}
@@ -154,6 +180,7 @@ export function SourceControlBranchContextRow({
))}
</span>
) : null}
<ManualReviewLinkButton url={manualReviewUrl} />
</div>
)
}
@@ -40,6 +40,7 @@ type SourceControlHeaderToolbarProps = {
branchSummary: GitBranchCompareSummary | null
compareBaseRef: string | null
upstreamStatus?: GitUpstreamStatus
manualReviewUrl?: string | null
}
function HostedReviewToolbarLink({
@@ -143,7 +144,8 @@ export function SourceControlHeaderToolbar({
onExpandNotes,
branchSummary,
compareBaseRef,
upstreamStatus
upstreamStatus,
manualReviewUrl
}: SourceControlHeaderToolbarProps): React.JSX.Element {
const filterInputRef = useRef<HTMLInputElement>(null)
const normalizedFilter = filterQuery.trim()
@@ -288,6 +290,7 @@ export function SourceControlHeaderToolbar({
summary={branchSummary}
compareBaseRef={compareBaseRef}
upstreamStatus={upstreamStatus}
manualReviewUrl={manualReviewUrl}
onChangeBaseRef={onChangeBaseRef}
onRetry={onRefreshBranchCompare}
/>
@@ -0,0 +1,173 @@
import { describe, expect, it } from 'vitest'
import { buildSourceControlManualReviewUrl } from './source-control-manual-review-url'
describe('buildSourceControlManualReviewUrl', () => {
it('builds a GitHub compare URL for the current remote branch', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'native-chat-does-not-auto-open',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@github.com:stablyai/orca.git',
upstreamName: 'origin/native-chat-does-not-auto-open'
})
).toBe(
'https://github.com/stablyai/orca/compare/main...native-chat-does-not-auto-open?expand=1'
)
})
it('qualifies GitHub fork heads when the push target remote differs from the base repo', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/upstream/main',
branchName: 'feature/fork-head',
repoRemoteName: 'upstream',
repoRemoteUrl: 'https://github.com/stablyai/orca.git',
pushTarget: {
remoteName: 'fork',
branchName: 'feature/fork-head',
remoteUrl: 'git@github.com:contributor/orca.git'
}
})
).toBe('https://github.com/stablyai/orca/compare/main...contributor:feature/fork-head?expand=1')
})
it('keeps slashes literal in a GitHub compare URL for a slash-containing branch name', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'slashdevcorpse/identifying-pwsh.exe-error',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@github.com:stablyai/orca.git',
upstreamName: 'origin/slashdevcorpse/identifying-pwsh.exe-error'
})
).toBe(
'https://github.com/stablyai/orca/compare/main...slashdevcorpse/identifying-pwsh.exe-error?expand=1'
)
})
it('builds a GitLab merge request URL for a self-hosted GitLab remote', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/release/next',
branchName: 'feature/gitlab',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@gitlab.company.test:group/sub/orca.git',
provider: 'gitlab',
upstreamName: 'origin/feature/gitlab'
})
).toBe(
'https://gitlab.company.test/group/sub/orca/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fgitlab&merge_request%5Btarget_branch%5D=release%2Fnext'
)
})
it('builds a Bitbucket manual pull request URL', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'origin/main',
branchName: 'feature/bitbucket',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@bitbucket.org:team/project.git',
upstreamName: 'origin/feature/bitbucket'
})
).toBe(
'https://bitbucket.org/team/project/pull-requests/new?source=feature%2Fbitbucket&dest=main'
)
})
it('builds an Azure DevOps pull request creation URL', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'feature/azure',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@ssh.dev.azure.com:v3/acme/widgets/widget-app.git',
upstreamName: 'origin/feature/azure'
})
).toBe(
'https://dev.azure.com/acme/widgets/_git/widget-app/pullrequestcreate?sourceRef=refs%2Fheads%2Ffeature%2Fazure&targetRef=refs%2Fheads%2Fmain'
)
})
it('builds a Gitea compare URL when the provider is known', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'feature/gitea',
repoRemoteName: 'origin',
repoRemoteUrl: 'ssh://git@gitea.company.test/team/orca.git',
provider: 'gitea',
upstreamName: 'origin/feature/gitea'
})
).toBe('https://gitea.company.test/team/orca/compare/main...feature/gitea')
})
it('suppresses the link when the branch tracks a fork remote with no resolvable push URL', () => {
// Repro: branch pushed only to a fork (tracked via plain git, so pushTarget
// is empty). A base-repo compare/main...quick-commands would 404 on origin.
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'quick-commands',
repoRemoteName: 'origin',
repoRemoteUrl: 'https://github.com/stablyai/orca.git',
upstreamName: 'pr-devajmeireles-orca/quick-commands'
})
).toBeNull()
})
it('uses the pushed upstream branch name when the local branch tracks a differently named branch on the base remote', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'local-wip-name',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@github.com:stablyai/orca.git',
upstreamName: 'origin/feature/pushed-name'
})
).toBe('https://github.com/stablyai/orca/compare/main...feature/pushed-name?expand=1')
})
it('still qualifies the fork head when Orca resolved the fork push URL', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'quick-commands',
repoRemoteName: 'origin',
repoRemoteUrl: 'https://github.com/stablyai/orca.git',
upstreamName: 'pr-devajmeireles-orca/quick-commands',
pushTarget: {
remoteName: 'pr-devajmeireles-orca',
branchName: 'quick-commands',
remoteUrl: 'git@github.com:devajmeireles/orca.git'
}
})
).toBe('https://github.com/stablyai/orca/compare/main...devajmeireles:quick-commands?expand=1')
})
it('does not guess a provider for unknown hosts without a provider hint', () => {
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'feature/unknown',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@git.company.test:team/orca.git',
upstreamName: 'origin/feature/unknown'
})
).toBeNull()
})
it('suppresses the link when the branch was never pushed (no upstream, no push target)', () => {
// Repro: local-only branch. main...<local-branch> lands on GitHub's
// "There isn't anything to compare" page.
expect(
buildSourceControlManualReviewUrl({
baseRef: 'refs/remotes/origin/main',
branchName: 'codex-runtime-home-refactor-design',
repoRemoteName: 'origin',
repoRemoteUrl: 'git@github.com:stablyai/orca.git',
upstreamName: null
})
).toBeNull()
})
})
@@ -0,0 +1,338 @@
import type { HostedReviewProvider } from '../../../../shared/hosted-review'
import type { GitPushTarget } from '../../../../shared/types'
type ManualReviewProvider = Exclude<HostedReviewProvider, 'unsupported'>
type RemoteRepoRef = {
provider: ManualReviewProvider | null
webBaseUrl: string
path: string
}
type ManualReviewUrlInput = {
baseRef: string | null | undefined
branchName: string | null | undefined
repoRemoteName?: string | null
repoRemoteUrl?: string | null
provider?: HostedReviewProvider | null
pushTarget?: GitPushTarget | null
/** Tracking upstream in `<remote>/<branch>` form (git `branch.upstream`). */
upstreamName?: string | null
}
type ParsedUpstream = { remoteName: string; branchName: string }
function parseUpstream(name: string | null | undefined): ParsedUpstream | null {
const trimmed = name?.trim()
if (!trimmed) {
return null
}
const slash = trimmed.indexOf('/')
if (slash <= 0 || slash === trimmed.length - 1) {
return null
}
return { remoteName: trimmed.slice(0, slash), branchName: trimmed.slice(slash + 1) }
}
function decodeSegment(value: string): string {
try {
return decodeURIComponent(value)
} catch {
return value
}
}
function encodePath(path: string): string {
return path.split('/').map(encodeURIComponent).join('/')
}
function cleanPath(path: string): string | null {
const parts = path
.replace(/^\/+/, '')
.replace(/\/+$/, '')
.replace(/\.git$/i, '')
.split('/')
.map((part) => part.trim())
.filter(Boolean)
.map(decodeSegment)
return parts.length >= 2 ? parts.join('/') : null
}
function providerForHost(host: string): ManualReviewProvider | null {
const normalized = host.toLowerCase()
if (normalized === 'github.com' || normalized === 'ssh.github.com') {
return 'github'
}
if (normalized === 'gitlab.com') {
return 'gitlab'
}
if (normalized === 'bitbucket.org') {
return 'bitbucket'
}
if (
normalized === 'dev.azure.com' ||
normalized === 'ssh.dev.azure.com' ||
normalized.endsWith('.visualstudio.com')
) {
return 'azure-devops'
}
return null
}
function normalizeProvider(
provider: HostedReviewProvider | null | undefined
): ManualReviewProvider | null {
return provider && provider !== 'unsupported' ? provider : null
}
function buildWebOrigin(protocol: string, host: string, hostname: string): string {
return protocol === 'http:' || protocol === 'https:'
? `${protocol}//${host}`
: `https://${hostname}`
}
function parseAzureDevOpsRemote(trimmed: string): RemoteRepoRef | null {
const scpLike = trimmed.match(/^(?:[^@/:]+@)?([^:\s/]+):([^\s]+?)(?:\.git)?$/)
if (scpLike && scpLike[1].toLowerCase() === 'ssh.dev.azure.com') {
const parts = cleanPath(scpLike[2])?.split('/') ?? []
if (parts.length >= 4 && parts[0].toLowerCase() === 'v3') {
const [, organization, project, repository] = parts
return {
provider: 'azure-devops',
path: `${organization}/${project}/_git/${repository}`,
webBaseUrl: `https://dev.azure.com/${encodePath(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`
}
}
}
try {
const url = new URL(trimmed)
const protocol = url.protocol.toLowerCase()
if (!['http:', 'https:', 'ssh:', 'git+ssh:'].includes(protocol)) {
return null
}
const host = url.hostname.toLowerCase()
const parts = cleanPath(url.pathname)?.split('/') ?? []
if (host === 'ssh.dev.azure.com' && parts.length >= 4 && parts[0].toLowerCase() === 'v3') {
const [, organization, project, repository] = parts
return {
provider: 'azure-devops',
path: `${organization}/${project}/_git/${repository}`,
webBaseUrl: `https://dev.azure.com/${encodePath(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`
}
}
const gitIndex = parts.findIndex((part) => part.toLowerCase() === '_git')
if (gitIndex < 1 || gitIndex + 1 >= parts.length) {
return null
}
const project = parts[gitIndex - 1]
const repository = parts[gitIndex + 1]
const prefix = parts.slice(0, gitIndex - 1)
const organization =
host === 'dev.azure.com' ? prefix[0] : host.endsWith('.visualstudio.com') ? null : undefined
const webPath =
organization === undefined
? [...prefix, project, '_git', repository]
: organization
? [organization, project, '_git', repository]
: [...prefix, project, '_git', repository]
return {
provider: 'azure-devops',
path: webPath.join('/'),
webBaseUrl: `${buildWebOrigin(protocol, url.host, url.hostname).replace(/\/+$/, '')}/${encodePath(webPath.join('/'))}`
}
} catch {
return null
}
}
function parseRemoteRepo(
remoteUrl: string,
providerHint?: HostedReviewProvider | null
): RemoteRepoRef | null {
const trimmed = remoteUrl.trim().replace(/^git\+/, '')
if (!trimmed || /^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.startsWith('/')) {
return null
}
const azure = parseAzureDevOpsRemote(trimmed)
if (azure && (providerHint == null || providerHint === 'azure-devops')) {
return azure
}
const scpLike = !trimmed.includes('://')
? trimmed.match(/^(?:[^@/:]+@)?([^:\s/]+):([^\s]+?)(?:\.git)?$/)
: null
if (scpLike) {
const host = scpLike[1].toLowerCase()
const path = cleanPath(scpLike[2])
if (!path) {
return null
}
const hintedProvider = normalizeProvider(providerHint)
return {
provider: hintedProvider ?? providerForHost(host) ?? null,
path,
webBaseUrl: `https://${host}/${encodePath(path)}`
}
}
try {
const url = new URL(trimmed)
const protocol = url.protocol.toLowerCase()
if (!['git:', 'http:', 'https:', 'ssh:'].includes(protocol)) {
return null
}
const path = cleanPath(url.pathname)
if (!path) {
return null
}
const host = url.hostname.toLowerCase()
const hintedProvider = normalizeProvider(providerHint)
const inferredProvider = providerForHost(host)
const provider = hintedProvider ?? inferredProvider
const webOrigin =
host === 'ssh.github.com'
? 'https://github.com'
: buildWebOrigin(protocol, url.host, url.hostname).replace(/\/+$/, '')
return {
provider,
path,
webBaseUrl: `${webOrigin}/${encodePath(path)}`
}
} catch {
return null
}
}
function branchFromRef(ref: string | null | undefined, remoteName?: string | null): string | null {
const trimmed = ref?.trim()
if (!trimmed) {
return null
}
const prefixes = remoteName
? [`refs/remotes/${remoteName}/`, `remotes/${remoteName}/`, `${remoteName}/`]
: ['refs/heads/']
for (const prefix of prefixes) {
if (trimmed.startsWith(prefix)) {
return trimmed.slice(prefix.length) || null
}
}
if (trimmed.startsWith('refs/remotes/')) {
const remoteAndBranch = trimmed.slice('refs/remotes/'.length)
const slashIndex = remoteAndBranch.indexOf('/')
return slashIndex > 0 ? remoteAndBranch.slice(slashIndex + 1) : null
}
if (trimmed.startsWith('remotes/')) {
const remoteAndBranch = trimmed.slice('remotes/'.length)
const slashIndex = remoteAndBranch.indexOf('/')
return slashIndex > 0 ? remoteAndBranch.slice(slashIndex + 1) : null
}
if (trimmed.startsWith('refs/heads/')) {
return trimmed.slice('refs/heads/'.length) || null
}
return trimmed
}
function githubHeadRef(base: RemoteRepoRef, head: RemoteRepoRef, branch: string): string {
if (base.path.toLowerCase() === head.path.toLowerCase()) {
return branch
}
const owner = head.path.split('/')[0]
return owner ? `${owner}:${branch}` : branch
}
function appendQuery(url: string, values: Record<string, string>): string {
const search = new URLSearchParams(values)
return `${url}?${search.toString()}`
}
// GitHub/Gitea compare refs keep '/' (slashed branch names like feature/foo) and
// ':' (the owner:branch fork qualifier) literal; percent-encoding those separators
// makes GitHub fail to resolve the branch. Only the segments between them are encoded.
function encodeCompareRef(ref: string): string {
return ref
.split(':')
.map((segment) => segment.split('/').map(encodeURIComponent).join('/'))
.join(':')
}
export function buildSourceControlManualReviewUrl(input: ManualReviewUrlInput): string | null {
const baseBranch = branchFromRef(input.baseRef, input.repoRemoteName)
const localBranch = input.branchName?.trim()
if (!baseBranch || !localBranch || localBranch === 'HEAD') {
return null
}
const baseRemoteUrl = input.repoRemoteUrl?.trim() || input.pushTarget?.remoteUrl?.trim()
const headRemoteUrl = input.pushTarget?.remoteUrl?.trim() || baseRemoteUrl
if (!baseRemoteUrl || !headRemoteUrl) {
return null
}
// Why: the tracking upstream is the source of truth for where the branch
// actually lives on the remote. When it tracks a remote we can't resolve to a
// URL (a fork tracked via plain git, so pushTarget is empty), a base-repo
// compare link would 404 — suppress instead of pointing at a missing branch.
const upstream = parseUpstream(input.upstreamName)
const baseRemoteName = input.repoRemoteName?.trim() || null
const hasResolvableForkUrl = Boolean(input.pushTarget?.remoteUrl?.trim())
if (
upstream &&
baseRemoteName &&
upstream.remoteName !== baseRemoteName &&
!hasResolvableForkUrl
) {
return null
}
const baseRepo = parseRemoteRepo(baseRemoteUrl, input.provider)
const headRepo = parseRemoteRepo(headRemoteUrl, input.provider)
if (!baseRepo || !headRepo) {
return null
}
// Why: without a pushTarget or tracking upstream there's no evidence the branch
// exists on any remote, so a compare/PR-create link would land on GitHub's
// "There isn't anything to compare" page. No link beats a broken one.
const pushedBranch = input.pushTarget?.branchName?.trim()
if (!pushedBranch && !upstream) {
return null
}
const provider = normalizeProvider(input.provider) ?? baseRepo.provider ?? headRepo.provider
// Prefer the pushed branch name (pushTarget, else the tracking upstream on the
// base remote) so the link matches what exists remotely, not the local name.
const headBranch =
pushedBranch ||
(upstream && (!baseRemoteName || upstream.remoteName === baseRemoteName)
? upstream.branchName
: localBranch)
switch (provider) {
case 'github':
return `${baseRepo.webBaseUrl}/compare/${encodeCompareRef(baseBranch)}...${encodeCompareRef(
githubHeadRef(baseRepo, headRepo, headBranch)
)}?expand=1`
case 'gitlab':
return appendQuery(`${baseRepo.webBaseUrl}/-/merge_requests/new`, {
'merge_request[source_branch]': headBranch,
'merge_request[target_branch]': baseBranch
})
case 'bitbucket':
return appendQuery(`${baseRepo.webBaseUrl}/pull-requests/new`, {
source: headBranch,
dest: baseBranch
})
case 'azure-devops':
return appendQuery(`${baseRepo.webBaseUrl}/pullrequestcreate`, {
sourceRef: `refs/heads/${headBranch}`,
targetRef: `refs/heads/${baseBranch}`
})
case 'gitea':
return `${baseRepo.webBaseUrl}/compare/${encodeCompareRef(baseBranch)}...${encodeCompareRef(headBranch)}`
default:
return null
}
}
+2 -3
View File
@@ -6401,8 +6401,6 @@
"a62b4cb39a": "Save Changes",
"29af933cd5": "New SSH Target",
"f2331ce599": "Edit SSH Target",
"7c13f58c91": "Until reset",
"55c56cf2c7": "Timeout after disconnect (seconds)",
"4a342f44c1": "Advanced Connection",
"e9609ddca6": "Proxy, jump host, and connection reuse",
"8c922dffba": "Reuse SSH connection for faster setup",
@@ -9165,7 +9163,8 @@
"b715ef615b": "{{value0}} commits ahead of {{value1}}",
"c1a8f3e204": "1 commit behind {{value0}}",
"d2b9g4f315": "{{value0}} commits behind {{value1}}",
"e9c2a5d416": "Even with {{value0}}"
"e9c2a5d416": "Even with {{value0}}",
"4b4a7de138": "Open review page in browser"
},
"SourceControlAgentActionDialog": {
"8e856842d1": "Could not start the selected agent.",
+2 -1
View File
@@ -9163,7 +9163,8 @@
"b715ef615b": "{{value0}} commits adelante de {{value1}}",
"c1a8f3e204": "1 commit detrás de {{value0}}",
"d2b9g4f315": "{{value0}} commits detrás de {{value1}}",
"e9c2a5d416": "A la par con {{value0}}"
"e9c2a5d416": "A la par con {{value0}}",
"4b4a7de138": "Open review page in browser"
},
"SourceControlAgentActionDialog": {
"8e856842d1": "No se pudo iniciar el agente seleccionado.",
+2 -1
View File
@@ -9163,7 +9163,8 @@
"b715ef615b": "{{value1}}より{{value0}}commits 進んでいます",
"c1a8f3e204": "{{value0}}より1commit 遅れています",
"d2b9g4f315": "{{value1}}より{{value0}}commits 遅れています",
"e9c2a5d416": "{{value0}}と同じ状態"
"e9c2a5d416": "{{value0}}と同じ状態",
"4b4a7de138": "Open review page in browser"
},
"SourceControlAgentActionDialog": {
"8e856842d1": "選択した agent を開始できませんでした。",
+2 -1
View File
@@ -9163,7 +9163,8 @@
"b715ef615b": "{{value1}}보다 {{value0}} commits 앞서 있음",
"c1a8f3e204": "{{value0}}보다 1 commit 뒤처짐",
"d2b9g4f315": "{{value1}}보다 {{value0}} commits 뒤처짐",
"e9c2a5d416": "{{value0}}와 동일한 상태"
"e9c2a5d416": "{{value0}}와 동일한 상태",
"4b4a7de138": "Open review page in browser"
},
"SourceControlAgentActionDialog": {
"8e856842d1": "선택한 agent를 시작할 수 없습니다.",
+2 -1
View File
@@ -9163,7 +9163,8 @@
"b715ef615b": "领先 {{value1}} {{value0}} 个提交",
"c1a8f3e204": "落后 {{value0}} 1 个提交",
"d2b9g4f315": "落后 {{value1}} {{value0}} 个提交",
"e9c2a5d416": "与 {{value0}} 同步"
"e9c2a5d416": "与 {{value0}} 同步",
"4b4a7de138": "Open review page in browser"
},
"SourceControlAgentActionDialog": {
"8e856842d1": "无法启动选定的智能体。",