Add copy link action for linked work items (#6140)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-06-23 00:25:56 -07:00 committed by GitHub
parent 17dee0861f
commit edf98f6578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 372 additions and 125 deletions

View File

@ -0,0 +1,147 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { CircleDot, Copy, Ellipsis, ExternalLink, MonitorUp, Pencil } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import {
WorktreeCardDetailSection,
WorktreeCardDetailSectionContent
} from './WorktreeCardDetailSection'
import { DetailHeader, MetadataActionIcon } from './WorktreeCardMetadataControls'
import { IssueStateBadge } from './WorktreeCardMetadataStatusBadges'
import type { WorktreeCardIssueDisplay } from './worktree-card-meta-types'
type WorktreeCardIssueDetailSectionProps = {
issue: WorktreeCardIssueDisplay | null
issueMenuOpen: boolean
onIssueMenuOpenChange: (open: boolean) => void
onCopyIssueLink?: () => void
onEditIssue?: (event: React.MouseEvent) => void
onOpenGitHubIssueInOrca?: (event: React.MouseEvent) => void
}
export function WorktreeCardIssueDetailSection({
issue,
issueMenuOpen,
onIssueMenuOpenChange,
onCopyIssueLink,
onEditIssue,
onOpenGitHubIssueInOrca
}: WorktreeCardIssueDetailSectionProps): React.JSX.Element | null {
if (!issue) {
return null
}
const issueLabels = issue.labels ?? []
const moreActionsLabel = translate(
'auto.components.sidebar.WorktreeCardMeta.moreIssueActions',
'More issue actions'
)
const moreActionsTrigger = (
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-6"
aria-label={moreActionsLabel}
onClick={(event) => event.stopPropagation()}
>
<Ellipsis className="size-3" />
</Button>
</DropdownMenuTrigger>
)
return (
<WorktreeCardDetailSection>
<DetailHeader
icon={<CircleDot className="size-3 text-muted-foreground" />}
label={translate(
'auto.components.sidebar.WorktreeCardMeta.e97d8f2876',
'Issue #{{value0}}',
{
value0: issue.number
}
)}
actions={
<>
{issue.url && onCopyIssueLink && (
<DropdownMenu modal={false} open={issueMenuOpen} onOpenChange={onIssueMenuOpenChange}>
{issueMenuOpen ? (
moreActionsTrigger
) : (
<Tooltip>
<TooltipTrigger asChild>{moreActionsTrigger}</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{moreActionsLabel}
</TooltipContent>
</Tooltip>
)}
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onSelect={onCopyIssueLink}>
<Copy className="size-3.5" />
{translate('auto.components.sidebar.WorktreeCardMeta.copyLink', 'Copy link')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{onEditIssue && (
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.807b13b9ec',
'Edit issue'
)}
onClick={onEditIssue}
>
<Pencil className="size-3" />
</MetadataActionIcon>
)}
{issue.url && onOpenGitHubIssueInOrca && (
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.2c67730e07',
'Open in Orca'
)}
onClick={onOpenGitHubIssueInOrca}
>
<MonitorUp className="size-3" />
</MetadataActionIcon>
)}
{issue.url && (
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.b22f058067',
'View on GitHub'
)}
href={issue.url}
>
<ExternalLink className="size-3" />
</MetadataActionIcon>
)}
</>
}
/>
<WorktreeCardDetailSectionContent className="space-y-1.5">
<div className="text-[13px] font-semibold leading-snug text-foreground break-words">
{issue.title}
</div>
{(issue.state || issueLabels.length > 0) && (
<div className="flex flex-wrap gap-1">
{issue.state && <IssueStateBadge state={issue.state} />}
{issueLabels.map((label) => (
<Badge key={label} variant="outline" className="h-4 px-1.5 text-[9px]">
{label}
</Badge>
))}
</div>
)}
</WorktreeCardDetailSectionContent>
</WorktreeCardDetailSection>
)
}

View File

@ -2,9 +2,14 @@
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { WorktreeCardDetailsHover } from './WorktreeCardMeta'
const toastMocks = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn()
}))
const interactionMocks = vi.hoisted(() => ({
hoverOpen: false,
onHoverOpenChange: undefined as ((open: boolean) => void) | undefined,
@ -13,6 +18,10 @@ const interactionMocks = vi.hoisted(() => ({
onUnlinkSelect: undefined as (() => void) | undefined
}))
vi.mock('sonner', () => ({
toast: toastMocks
}))
vi.mock('@/components/ui/hover-card', () => ({
HoverCard: ({
children,
@ -78,6 +87,7 @@ const reviewFixture = {
describe('WorktreeCardDetailsHover interactions', () => {
let container: HTMLDivElement
let root: Root
const writeClipboardText = vi.fn()
afterEach(() => {
act(() => {
@ -89,6 +99,21 @@ describe('WorktreeCardDetailsHover interactions', () => {
interactionMocks.onHoverOpenChange = undefined
interactionMocks.onReviewMenuOpenChange = undefined
interactionMocks.onUnlinkSelect = undefined
writeClipboardText.mockReset()
toastMocks.success.mockReset()
toastMocks.error.mockReset()
})
beforeEach(() => {
Object.defineProperty(window, 'api', {
configurable: true,
value: {
ui: {
writeClipboardText
}
}
})
writeClipboardText.mockResolvedValue(undefined)
})
function renderHover(onUnlinkReview = vi.fn()): ReturnType<typeof vi.fn> {
@ -142,16 +167,14 @@ describe('WorktreeCardDetailsHover interactions', () => {
)
})
it('suppresses the tooltip while the review menu is open', () => {
it('omits the review trigger tooltip while the review menu is open', () => {
renderHover()
act(() => {
interactionMocks.onReviewMenuOpenChange?.(true)
})
expect(container.querySelector('[data-tooltip-open]')?.getAttribute('data-tooltip-open')).toBe(
'false'
)
expect(container.textContent).not.toContain('More PR actions')
})
it('invokes unlink and closes the hover from the menu item', () => {
@ -178,4 +201,55 @@ describe('WorktreeCardDetailsHover interactions', () => {
container.querySelector('[data-review-menu-open]')?.getAttribute('data-review-menu-open')
).toBe('false')
})
it('copies the review URL and closes the hover from the menu item', async () => {
const onUnlinkReview = renderHover()
act(() => {
interactionMocks.onHoverOpenChange?.(true)
interactionMocks.onReviewMenuOpenChange?.(true)
})
const copyButton = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Copy link')
)
await act(async () => {
copyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await Promise.resolve()
})
expect(writeClipboardText).toHaveBeenCalledWith('https://github.com/acme/orca/pull/456')
expect(onUnlinkReview).not.toHaveBeenCalled()
expect(toastMocks.success).toHaveBeenCalledWith('PR link copied')
expect(container.querySelector('[data-hover-open]')?.getAttribute('data-hover-open')).toBe(
'false'
)
expect(
container.querySelector('[data-review-menu-open]')?.getAttribute('data-review-menu-open')
).toBe('false')
})
it('reports clipboard failures without unlinking the review', async () => {
writeClipboardText.mockRejectedValueOnce(new Error('clipboard unavailable'))
const onUnlinkReview = renderHover()
act(() => {
interactionMocks.onHoverOpenChange?.(true)
interactionMocks.onReviewMenuOpenChange?.(true)
})
const copyButton = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Copy link')
)
await act(async () => {
copyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await Promise.resolve()
})
expect(writeClipboardText).toHaveBeenCalledWith('https://github.com/acme/orca/pull/456')
expect(onUnlinkReview).not.toHaveBeenCalled()
expect(toastMocks.error).toHaveBeenCalledWith('Failed to copy link')
})
})

View File

@ -109,13 +109,15 @@ describe('WorktreeCardDetailsHover', () => {
expect(moreActionsIndex).toBeGreaterThan(-1)
expect(markup).toContain('More PR actions')
expect(markup).toContain('Copy link')
expect(markup).toContain('Unlink PR')
expect(moreActionsIndex).toBeLessThan(openInOrcaIndex)
expect(openInOrcaIndex).toBeLessThan(viewOnGitHubIndex)
expect(markup).not.toContain('aria-label="Unlink PR"')
expect(markup.indexOf('Copy link')).toBeLessThan(markup.indexOf('Unlink PR'))
})
it('puts issue edit before open actions and keeps GitHub last', () => {
it('puts issue copy menu before edit and open actions and keeps GitHub last', () => {
const markup = renderToStaticMarkup(
<WorktreeCardDetailsHover
issue={{
@ -136,11 +138,17 @@ describe('WorktreeCardDetailsHover', () => {
</WorktreeCardDetailsHover>
)
const moreActionsIndex = markup.indexOf('aria-label="More issue actions"')
const copyLinkIndex = markup.indexOf('Copy link')
const editIssueIndex = markup.indexOf('aria-label="Edit issue"')
const openInOrcaIndex = markup.indexOf('aria-label="Open in Orca"')
const viewOnGitHubIndex = markup.indexOf('aria-label="View on GitHub"')
expect(moreActionsIndex).toBeGreaterThan(-1)
expect(copyLinkIndex).toBeGreaterThan(-1)
expect(editIssueIndex).toBeGreaterThan(-1)
expect(moreActionsIndex).toBeLessThan(editIssueIndex)
expect(copyLinkIndex).toBeLessThan(editIssueIndex)
expect(editIssueIndex).toBeLessThan(openInOrcaIndex)
expect(openInOrcaIndex).toBeLessThan(viewOnGitHubIndex)
})

View File

@ -2,6 +2,7 @@ import React from 'react'
import { Badge } from '@/components/ui/badge'
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card'
import { CalendarClock, CircleDot, ExternalLink, MonitorUp, Pencil, StickyNote } from 'lucide-react'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { LinearIcon } from '@/components/icons/LinearIcon'
import { SelectedTextCopyMenu } from '@/components/SelectedTextCopyMenu'
@ -12,7 +13,7 @@ import {
WorktreeCardDetailSectionContent
} from './WorktreeCardDetailSection'
import { DetailHeader, MetaIconBadge, MetadataActionIcon } from './WorktreeCardMetadataControls'
import { IssueStateBadge, LinearStateBadge } from './WorktreeCardMetadataStatusBadges'
import { LinearStateBadge } from './WorktreeCardMetadataStatusBadges'
import { useWorktreeCardDetailsHoverControl } from './worktree-card-details-hover-state'
import { getReviewLabel, ReviewIcon } from './worktree-review-helpers'
import type {
@ -25,6 +26,7 @@ import type {
import { translate } from '@/i18n/i18n'
import { WorktreeCardReviewDetailSection } from './WorktreeCardReviewDetailSection'
import { WorktreeCardAutomationDetailSection } from './WorktreeCardAutomationDetailSection'
import { WorktreeCardIssueDetailSection } from './WorktreeCardIssueDetailSection'
export type {
WorktreeCardIssueDisplay,
@ -155,8 +157,10 @@ export function WorktreeCardDetailsHover({
const internalHoverControl = useWorktreeCardDetailsHoverControl()
const {
hoverOpen,
issueMenuOpen,
reviewMenuOpen,
handleHoverOpenChange,
handleIssueMenuOpenChange,
handleReviewMenuOpenChange,
closeHover
} = hoverControl ?? internalHoverControl
@ -167,6 +171,43 @@ export function WorktreeCardDetailsHover({
},
[closeHover]
)
const copyLinkedWorkItemLink = React.useCallback(async (url: string, label: string) => {
try {
// Why: Electron clipboard IPC remains reliable from nested hover/dropdown
// overlays where browser clipboard activation can be lost.
await window.api.ui.writeClipboardText(url)
toast.success(
translate('auto.components.sidebar.WorktreeCardMeta.copyLinkSuccess', '{{value0}} copied', {
value0: label
})
)
} catch {
toast.error(
translate('auto.components.sidebar.WorktreeCardMeta.copyLinkFailure', 'Failed to copy link')
)
}
}, [])
const handleCopyIssueLink = React.useCallback((): void => {
if (!issue?.url) {
return
}
closeHover()
void copyLinkedWorkItemLink(
issue.url,
translate('auto.components.sidebar.WorktreeCardMeta.issueLinkLabel', 'Issue link')
)
}, [closeHover, copyLinkedWorkItemLink, issue?.url])
const handleCopyReviewLink = React.useCallback((): void => {
if (!review?.url) {
return
}
void copyLinkedWorkItemLink(
review.url,
translate('auto.components.sidebar.WorktreeCardMeta.reviewLinkLabel', '{{value0}} link', {
value0: getReviewLabel(review)
})
)
}, [copyLinkedWorkItemLink, review])
const showIdentityHeader = Boolean(branchName || workspaceTitle)
@ -178,7 +219,6 @@ export function WorktreeCardDetailsHover({
return children
}
const issueLabels = issue?.labels ?? []
const branchIdentity = branchName ? (
<div
className={cn(
@ -230,70 +270,16 @@ export function WorktreeCardDetailsHover({
</div>
)}
{issue && (
<WorktreeCardDetailSection>
<DetailHeader
icon={<CircleDot className="size-3 text-muted-foreground" />}
label={translate(
'auto.components.sidebar.WorktreeCardMeta.e97d8f2876',
'Issue #{{value0}}',
{ value0: issue.number }
)}
actions={
<>
{onEditIssue && (
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.807b13b9ec',
'Edit issue'
)}
onClick={onEditIssue}
>
<Pencil className="size-3" />
</MetadataActionIcon>
)}
{issue.url && onOpenGitHubIssueInOrca && (
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.2c67730e07',
'Open in Orca'
)}
onClick={dismissAndRun(onOpenGitHubIssueInOrca)}
>
<MonitorUp className="size-3" />
</MetadataActionIcon>
)}
{issue.url && (
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.b22f058067',
'View on GitHub'
)}
href={issue.url}
>
<ExternalLink className="size-3" />
</MetadataActionIcon>
)}
</>
}
/>
<WorktreeCardDetailSectionContent className="space-y-1.5">
<div className="text-[13px] font-semibold leading-snug text-foreground break-words">
{issue.title}
</div>
{(issue.state || issueLabels.length > 0) && (
<div className="flex flex-wrap gap-1">
{issue.state && <IssueStateBadge state={issue.state} />}
{issueLabels.map((label) => (
<Badge key={label} variant="outline" className="h-4 px-1.5 text-[9px]">
{label}
</Badge>
))}
</div>
)}
</WorktreeCardDetailSectionContent>
</WorktreeCardDetailSection>
)}
<WorktreeCardIssueDetailSection
issue={issue}
issueMenuOpen={issueMenuOpen}
onIssueMenuOpenChange={handleIssueMenuOpenChange}
onCopyIssueLink={issue?.url ? handleCopyIssueLink : undefined}
onEditIssue={onEditIssue}
onOpenGitHubIssueInOrca={
onOpenGitHubIssueInOrca ? dismissAndRun(onOpenGitHubIssueInOrca) : undefined
}
/>
{linearIssue && (
<WorktreeCardDetailSection>
@ -357,6 +343,7 @@ export function WorktreeCardDetailsHover({
reviewMenuOpen={reviewMenuOpen}
onReviewMenuOpenChange={handleReviewMenuOpenChange}
onOpenReviewInOrca={onOpenReviewInOrca}
onCopyReviewLink={review?.url ? handleCopyReviewLink : undefined}
onUnlinkReview={onUnlinkReview}
closeHover={closeHover}
/>

View File

@ -7,7 +7,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Ellipsis, ExternalLink, MonitorUp, Unlink } from 'lucide-react'
import { Copy, Ellipsis, ExternalLink, MonitorUp, Unlink } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import {
WorktreeCardDetailSection,
@ -23,6 +23,7 @@ type WorktreeCardReviewDetailSectionProps = {
reviewMenuOpen: boolean
onReviewMenuOpenChange: (open: boolean) => void
onOpenReviewInOrca?: (event: React.MouseEvent) => void
onCopyReviewLink?: () => void
onUnlinkReview?: () => void
closeHover: () => void
}
@ -32,6 +33,7 @@ export function WorktreeCardReviewDetailSection({
reviewMenuOpen,
onReviewMenuOpenChange,
onOpenReviewInOrca,
onCopyReviewLink,
onUnlinkReview,
closeHover
}: WorktreeCardReviewDetailSectionProps): React.JSX.Element | null {
@ -41,6 +43,25 @@ export function WorktreeCardReviewDetailSection({
const reviewLabel = getReviewLabel(review)
const reviewProvider = getProviderName(review)
const moreActionsLabel = translate(
'auto.components.sidebar.WorktreeCardMeta.dbe2d18972',
'More {{value0}} actions',
{ value0: reviewLabel }
)
const moreActionsTrigger = (
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-6"
aria-label={moreActionsLabel}
onClick={(event) => event.stopPropagation()}
>
<Ellipsis className="size-3" />
</Button>
</DropdownMenuTrigger>
)
const dismissAndOpenReview = (event: React.MouseEvent): void => {
closeHover()
onOpenReviewInOrca?.(event)
@ -57,53 +78,52 @@ export function WorktreeCardReviewDetailSection({
)}
actions={
<>
{onUnlinkReview && (
{(onCopyReviewLink || onUnlinkReview) && (
<DropdownMenu
modal={false}
open={reviewMenuOpen}
onOpenChange={onReviewMenuOpenChange}
>
<Tooltip open={reviewMenuOpen ? false : undefined}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-6"
aria-label={translate(
'auto.components.sidebar.WorktreeCardMeta.dbe2d18972',
'More {{value0}} actions',
{ value0: reviewLabel }
)}
onClick={(event) => event.stopPropagation()}
>
<Ellipsis className="size-3" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate(
'auto.components.sidebar.WorktreeCardMeta.dbe2d18972',
'More {{value0}} actions',
{ value0: reviewLabel }
)}
</TooltipContent>
</Tooltip>
{reviewMenuOpen ? (
moreActionsTrigger
) : (
<Tooltip>
<TooltipTrigger asChild>{moreActionsTrigger}</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{moreActionsLabel}
</TooltipContent>
</Tooltip>
)}
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
onSelect={() => {
closeHover()
onUnlinkReview?.()
}}
>
<Unlink className="size-3.5" />
{translate(
'auto.components.sidebar.WorktreeCardMeta.ae76907ca6',
'Unlink {{value0}}',
{ value0: reviewLabel }
)}
</DropdownMenuItem>
{onCopyReviewLink && (
<DropdownMenuItem
onSelect={() => {
closeHover()
onCopyReviewLink()
}}
>
<Copy className="size-3.5" />
{translate(
'auto.components.sidebar.WorktreeCardReviewDetailSection.copyLink',
'Copy link'
)}
</DropdownMenuItem>
)}
{onUnlinkReview && (
<DropdownMenuItem
onSelect={() => {
closeHover()
onUnlinkReview()
}}
>
<Unlink className="size-3.5" />
{translate(
'auto.components.sidebar.WorktreeCardMeta.ae76907ca6',
'Unlink {{value0}}',
{ value0: reviewLabel }
)}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}

View File

@ -1,32 +1,34 @@
import { useCallback, useRef, useState } from 'react'
type WorktreeCardDetailMenu = 'issue' | 'review'
export function useWorktreeCardDetailsHoverControl() {
const [open, setOpen] = useState(false)
const [reviewMenuOpen, setReviewMenuOpen] = useState(false)
const [openMenu, setOpenMenu] = useState<WorktreeCardDetailMenu | null>(null)
const pendingHoverCloseRef = useRef(false)
const closeHover = useCallback(() => {
pendingHoverCloseRef.current = false
setReviewMenuOpen(false)
setOpenMenu(null)
setOpen(false)
}, [])
const handleHoverOpenChange = useCallback(
(next: boolean) => {
// Why: the portaled PR menu sits outside HoverCardContent — keep the card
// mounted until the menu closes so the unlink item stays clickable.
if (reviewMenuOpen) {
// Why: portaled detail menus sit outside HoverCardContent — keep the card
// mounted until the menu closes so the menu items stay clickable.
if (openMenu) {
pendingHoverCloseRef.current = !next
return
}
pendingHoverCloseRef.current = false
setOpen(next)
},
[reviewMenuOpen]
[openMenu]
)
const handleReviewMenuOpenChange = useCallback((next: boolean) => {
setReviewMenuOpen(next)
const setDetailMenuOpen = useCallback((menu: WorktreeCardDetailMenu, next: boolean) => {
setOpenMenu(next ? menu : null)
if (!next && pendingHoverCloseRef.current) {
pendingHoverCloseRef.current = false
setOpen(false)
@ -34,10 +36,12 @@ export function useWorktreeCardDetailsHoverControl() {
}, [])
return {
hoverOpen: open || reviewMenuOpen,
reviewMenuOpen,
hoverOpen: open || Boolean(openMenu),
issueMenuOpen: openMenu === 'issue',
reviewMenuOpen: openMenu === 'review',
handleHoverOpenChange,
handleReviewMenuOpenChange,
handleIssueMenuOpenChange: (next: boolean) => setDetailMenuOpen('issue', next),
handleReviewMenuOpenChange: (next: boolean) => setDetailMenuOpen('review', next),
closeHover
}
}

View File

@ -3833,6 +3833,7 @@
"1b0a156717": "Agents"
},
"WorktreeCardReviewDetailSection": {
"copyLink": "Copy link",
"reviewHeader": "{{value0}} #{{value1}}"
},
"WorktreeCardMeta": {
@ -3849,6 +3850,12 @@
"807b13b9ec": "Edit issue",
"b22f058067": "View on GitHub",
"e97d8f2876": "Issue #{{value0}}",
"moreIssueActions": "More issue actions",
"copyLink": "Copy link",
"copyLinkSuccess": "{{value0}} copied",
"copyLinkFailure": "Failed to copy link",
"issueLinkLabel": "Issue link",
"reviewLinkLabel": "{{value0}} link",
"3ea2702e62": "Linked {{value0}} #{{value1}}",
"b105fd3057": "Linked Linear {{value0}}",
"3f2649eeb8": "Linked issue #{{value0}}",