mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Add copy action for markdown review notes
This commit is contained in:
@@ -137,7 +137,7 @@
|
||||
}
|
||||
|
||||
.rich-markdown-review-rail-toggle,
|
||||
.rich-markdown-review-rail-send {
|
||||
.rich-markdown-review-rail-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -156,20 +156,20 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rich-markdown-review-rail-send {
|
||||
.rich-markdown-review-rail-action {
|
||||
width: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rich-markdown-review-rail-toggle:hover,
|
||||
.rich-markdown-review-rail-toggle[aria-expanded='true'],
|
||||
.rich-markdown-review-rail-send:hover:not(:disabled) {
|
||||
.rich-markdown-review-rail-action:hover:not(:disabled) {
|
||||
background: var(--accent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.rich-markdown-review-rail-send:disabled,
|
||||
.rich-markdown-review-note-send:disabled {
|
||||
.rich-markdown-review-rail-action:disabled,
|
||||
.rich-markdown-review-note-action:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.45;
|
||||
}
|
||||
@@ -220,7 +220,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rich-markdown-review-note-send {
|
||||
.rich-markdown-review-note-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -235,7 +235,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rich-markdown-review-note-send:hover:not(:disabled) {
|
||||
.rich-markdown-review-note-action:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--foreground) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--foreground) 6%, transparent);
|
||||
color: var(--foreground);
|
||||
|
||||
@@ -83,6 +83,7 @@ import {
|
||||
sortMarkdownReviewNotes,
|
||||
type MarkdownReviewNote
|
||||
} from '@/lib/markdown-review-notes'
|
||||
import { copyMarkdownReviewNotesForAgent } from '@/lib/markdown-review-note-copy'
|
||||
import { NotesSendMenu, type NotesSendMenuScope } from './NotesSendMenu'
|
||||
import { findWorktreeById } from '@/store/slices/worktree-helpers'
|
||||
import { dirname } from '@/lib/path'
|
||||
@@ -557,7 +558,9 @@ export default function MarkdownPreview({
|
||||
}, [frontMatter])
|
||||
const [activeAnnotationBlockKey, setActiveAnnotationBlockKey] = useState<string | null>(null)
|
||||
const [reviewNotesCopied, setReviewNotesCopied] = useState(false)
|
||||
const [copiedReviewNoteId, setCopiedReviewNoteId] = useState<string | null>(null)
|
||||
const reviewNotesCopiedResetTimerRef = useRef<number | null>(null)
|
||||
const copiedReviewNoteResetTimerRef = useRef<number | null>(null)
|
||||
// Why: clipboard IPC can resolve after the preview unmounts; skip copied
|
||||
// feedback instead of starting a reset timer on a stale preview.
|
||||
const reviewNotesCopyMountedRef = useRef(false)
|
||||
@@ -568,10 +571,6 @@ export default function MarkdownPreview({
|
||||
() => sortMarkdownReviewNotes(markdownComments as MarkdownReviewNote[]),
|
||||
[markdownComments]
|
||||
)
|
||||
const markdownReviewPrompt = useMemo(
|
||||
() => formatMarkdownReviewNotes(markdownReviewNotes, renderedContent),
|
||||
[markdownReviewNotes, renderedContent]
|
||||
)
|
||||
const unsentMarkdownReviewNotes = useMemo(
|
||||
() => markdownReviewNotes.filter((note) => !note.sentAt),
|
||||
[markdownReviewNotes]
|
||||
@@ -703,13 +702,21 @@ export default function MarkdownPreview({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearCopiedReviewNoteResetTimer = useCallback((): void => {
|
||||
if (copiedReviewNoteResetTimerRef.current !== null) {
|
||||
window.clearTimeout(copiedReviewNoteResetTimerRef.current)
|
||||
copiedReviewNoteResetTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const cleanupPreviewSurfaceTimers = useCallback((): void => {
|
||||
// Why: reveal/copy timers are event-owned, but the final cancellation
|
||||
// belongs to the preview surface unmount.
|
||||
cancelMarkdownPreviewEditorRevealFrames(pendingEditorRevealFrameIdsRef)
|
||||
clearMarkdownPreviewTimeout(attentionReviewCommentTimeoutRef)
|
||||
clearReviewNotesCopiedResetTimer()
|
||||
}, [clearReviewNotesCopiedResetTimer])
|
||||
clearCopiedReviewNoteResetTimer()
|
||||
}, [clearCopiedReviewNoteResetTimer, clearReviewNotesCopiedResetTimer])
|
||||
|
||||
const setRootRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
@@ -848,8 +855,12 @@ export default function MarkdownPreview({
|
||||
return
|
||||
}
|
||||
try {
|
||||
await window.api.ui.writeClipboardText(markdownReviewPrompt)
|
||||
if (!reviewNotesCopyMountedRef.current) {
|
||||
const copied = await copyMarkdownReviewNotesForAgent({
|
||||
notes: markdownReviewNotes,
|
||||
content: renderedContent,
|
||||
writeClipboardText: window.api.ui.writeClipboardText
|
||||
})
|
||||
if (!copied || !reviewNotesCopyMountedRef.current) {
|
||||
return
|
||||
}
|
||||
clearReviewNotesCopiedResetTimer()
|
||||
@@ -861,7 +872,31 @@ export default function MarkdownPreview({
|
||||
} catch {
|
||||
// Best-effort clipboard action; failures usually mean the window is not focused.
|
||||
}
|
||||
}, [clearReviewNotesCopiedResetTimer, markdownReviewNotes.length, markdownReviewPrompt])
|
||||
}, [clearReviewNotesCopiedResetTimer, markdownReviewNotes, renderedContent])
|
||||
|
||||
const handleCopyMarkdownReviewNote = useCallback(
|
||||
async (note: MarkdownReviewNote): Promise<void> => {
|
||||
try {
|
||||
const copied = await copyMarkdownReviewNotesForAgent({
|
||||
notes: [note],
|
||||
content: renderedContent,
|
||||
writeClipboardText: window.api.ui.writeClipboardText
|
||||
})
|
||||
if (!copied || !reviewNotesCopyMountedRef.current) {
|
||||
return
|
||||
}
|
||||
clearCopiedReviewNoteResetTimer()
|
||||
setCopiedReviewNoteId(note.id)
|
||||
copiedReviewNoteResetTimerRef.current = window.setTimeout(() => {
|
||||
copiedReviewNoteResetTimerRef.current = null
|
||||
setCopiedReviewNoteId(null)
|
||||
}, 1600)
|
||||
} catch {
|
||||
// Best-effort clipboard action; failures usually mean the window is not focused.
|
||||
}
|
||||
},
|
||||
[clearCopiedReviewNoteResetTimer, renderedContent]
|
||||
)
|
||||
|
||||
const pulseRenderedMarkdownReviewNote = useCallback((commentId: string): void => {
|
||||
if (attentionReviewCommentTimeoutRef.current !== null) {
|
||||
@@ -1029,16 +1064,39 @@ export default function MarkdownPreview({
|
||||
onDelete={() => void deleteDiffComment(sourceWorktree.id, comment.id)}
|
||||
onSubmitEdit={(body) => updateDiffComment(sourceWorktree.id, comment.id, body)}
|
||||
headerActions={
|
||||
<MarkdownSingleNoteSendMenu
|
||||
worktreeId={sourceWorktree.id}
|
||||
filePath={filePath}
|
||||
content={renderedContent}
|
||||
note={comment as MarkdownReviewNote}
|
||||
modeSlot="preview-inline"
|
||||
onDelivered={(notes) =>
|
||||
void clearDeliveredDiffComments(sourceWorktree.id, notes)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="orca-diff-comment-pill-btn"
|
||||
title={
|
||||
copiedReviewNoteId === comment.id ? 'Copied note' : 'Copy note for agent'
|
||||
}
|
||||
aria-label={
|
||||
copiedReviewNoteId === comment.id ? 'Copied note' : 'Copy note for agent'
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void handleCopyMarkdownReviewNote(comment as MarkdownReviewNote)
|
||||
}}
|
||||
>
|
||||
{copiedReviewNoteId === comment.id ? (
|
||||
<Check className="size-3" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
</button>
|
||||
<MarkdownSingleNoteSendMenu
|
||||
worktreeId={sourceWorktree.id}
|
||||
filePath={filePath}
|
||||
content={renderedContent}
|
||||
note={comment as MarkdownReviewNote}
|
||||
modeSlot="preview-inline"
|
||||
onDelivered={(notes) =>
|
||||
void clearDeliveredDiffComments(sourceWorktree.id, notes)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -1053,9 +1111,11 @@ export default function MarkdownPreview({
|
||||
attentionReviewCommentId,
|
||||
addDiffComment,
|
||||
clearDeliveredDiffComments,
|
||||
copiedReviewNoteId,
|
||||
deleteDiffComment,
|
||||
filePath,
|
||||
getMarkdownCommentsForRange,
|
||||
handleCopyMarkdownReviewNote,
|
||||
markdownAnnotationsEnabled,
|
||||
content,
|
||||
renderedContent,
|
||||
@@ -1755,7 +1815,7 @@ function MarkdownSingleNoteSendMenu({
|
||||
}
|
||||
]}
|
||||
targetModeLabel="This note"
|
||||
triggerClassName="markdown-review-icon-button"
|
||||
triggerClassName="orca-diff-comment-pill-btn"
|
||||
disabledTooltip="Note already sent"
|
||||
onDelivered={onDelivered}
|
||||
/>
|
||||
|
||||
@@ -57,13 +57,14 @@ import { DiffCommentPopover } from '../diff-comments/DiffCommentPopover'
|
||||
import { DiffCommentCard } from '../diff-comments/DiffCommentCard'
|
||||
import { NotesSendMenu, type NotesSendMenuScope } from './NotesSendMenu'
|
||||
import { isMarkdownComment } from '@/lib/diff-comment-compat'
|
||||
import { MessageSquare, Plus } from 'lucide-react'
|
||||
import { Check, Copy, MessageSquare, Plus } from 'lucide-react'
|
||||
import {
|
||||
formatMarkdownReviewNotes,
|
||||
getMarkdownReviewCardQuote,
|
||||
sortMarkdownReviewNotes,
|
||||
type MarkdownReviewNote
|
||||
} from '@/lib/markdown-review-notes'
|
||||
import { copyMarkdownReviewNotesForAgent } from '@/lib/markdown-review-note-copy'
|
||||
import {
|
||||
richMarkdownAnnotationHighlightPluginKey,
|
||||
type RichMarkdownAnnotationHighlightRange
|
||||
@@ -650,6 +651,8 @@ export default function RichMarkdownEditor({
|
||||
null
|
||||
)
|
||||
const [reviewRailOpen, setReviewRailOpen] = useState(false)
|
||||
const [reviewNotesCopied, setReviewNotesCopied] = useState(false)
|
||||
const [copiedReviewNoteId, setCopiedReviewNoteId] = useState<string | null>(null)
|
||||
const [activeReviewCommentId, setActiveReviewCommentId] = useState<string | null>(null)
|
||||
const [attentionReviewCommentId, setAttentionReviewCommentId] = useState<string | null>(null)
|
||||
const [notePositions, setNotePositions] = useState<RichMarkdownReviewNotePosition[]>([])
|
||||
@@ -660,6 +663,8 @@ export default function RichMarkdownEditor({
|
||||
const markdownSourceLineOffsetRef = useRef(markdownSourceLineOffset)
|
||||
const attentionReviewCommentTimeoutRef = useRef<number | null>(null)
|
||||
const sourceAttentionTimeoutRef = useRef<number | null>(null)
|
||||
const reviewNotesCopiedResetTimerRef = useRef<number | null>(null)
|
||||
const copiedReviewNoteResetTimerRef = useRef<number | null>(null)
|
||||
const annotationTargetFrameRef = useRef<number | null>(null)
|
||||
const notePositionsFrameRef = useRef<number | null>(null)
|
||||
const isEditingLinkRef = useRef(false)
|
||||
@@ -803,16 +808,28 @@ export default function RichMarkdownEditor({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearReviewCopyTimers = useCallback(() => {
|
||||
if (reviewNotesCopiedResetTimerRef.current !== null) {
|
||||
window.clearTimeout(reviewNotesCopiedResetTimerRef.current)
|
||||
reviewNotesCopiedResetTimerRef.current = null
|
||||
}
|
||||
if (copiedReviewNoteResetTimerRef.current !== null) {
|
||||
window.clearTimeout(copiedReviewNoteResetTimerRef.current)
|
||||
copiedReviewNoteResetTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setRootElement = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
// Why: review-note pulses are tied to this editor root; ref cleanup
|
||||
// keeps the existing unmount boundary without a passive Effect.
|
||||
// Why: review-note pulses and copy feedback are tied to this editor root;
|
||||
// ref cleanup keeps the unmount boundary out of passive Effects.
|
||||
if (node === null) {
|
||||
clearAttentionTimers()
|
||||
clearReviewCopyTimers()
|
||||
}
|
||||
rootRef.current = node
|
||||
},
|
||||
[clearAttentionTimers]
|
||||
[clearAttentionTimers, clearReviewCopyTimers]
|
||||
)
|
||||
|
||||
const syncAnnotationTarget = useCallback((nextEditor: Editor): void => {
|
||||
@@ -856,6 +873,60 @@ export default function RichMarkdownEditor({
|
||||
})
|
||||
}, [])
|
||||
|
||||
const markReviewNotesCopied = useCallback((): void => {
|
||||
clearReviewCopyTimers()
|
||||
setCopiedReviewNoteId(null)
|
||||
setReviewNotesCopied(true)
|
||||
reviewNotesCopiedResetTimerRef.current = window.setTimeout(() => {
|
||||
reviewNotesCopiedResetTimerRef.current = null
|
||||
setReviewNotesCopied(false)
|
||||
}, 1600)
|
||||
}, [clearReviewCopyTimers])
|
||||
|
||||
const markReviewNoteCopied = useCallback((noteId: string): void => {
|
||||
if (copiedReviewNoteResetTimerRef.current !== null) {
|
||||
window.clearTimeout(copiedReviewNoteResetTimerRef.current)
|
||||
}
|
||||
setCopiedReviewNoteId(noteId)
|
||||
copiedReviewNoteResetTimerRef.current = window.setTimeout(() => {
|
||||
copiedReviewNoteResetTimerRef.current = null
|
||||
setCopiedReviewNoteId(null)
|
||||
}, 1600)
|
||||
}, [])
|
||||
|
||||
const handleCopyMarkdownReviewNotes = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const copied = await copyMarkdownReviewNotesForAgent({
|
||||
notes: markdownReviewNotes,
|
||||
content: markdownReviewContent,
|
||||
writeClipboardText: window.api.ui.writeClipboardText
|
||||
})
|
||||
if (copied && rootRef.current) {
|
||||
markReviewNotesCopied()
|
||||
}
|
||||
} catch {
|
||||
// Best-effort clipboard action; failures usually mean the window is not focused.
|
||||
}
|
||||
}, [markdownReviewContent, markdownReviewNotes, markReviewNotesCopied])
|
||||
|
||||
const handleCopyMarkdownReviewNote = useCallback(
|
||||
async (note: MarkdownReviewNote): Promise<void> => {
|
||||
try {
|
||||
const copied = await copyMarkdownReviewNotesForAgent({
|
||||
notes: [note],
|
||||
content: markdownReviewContent,
|
||||
writeClipboardText: window.api.ui.writeClipboardText
|
||||
})
|
||||
if (copied && rootRef.current) {
|
||||
markReviewNoteCopied(note.id)
|
||||
}
|
||||
} catch {
|
||||
// Best-effort clipboard action; failures usually mean the window is not focused.
|
||||
}
|
||||
},
|
||||
[markdownReviewContent, markReviewNoteCopied]
|
||||
)
|
||||
|
||||
const syncNotePositions = useCallback((): void => {
|
||||
const ed = editorRef.current
|
||||
const container = scrollContainerRef.current
|
||||
@@ -1795,28 +1866,62 @@ export default function RichMarkdownEditor({
|
||||
onSubmitEdit={(body) => updateDiffComment(worktreeId, comment.id, body)}
|
||||
onContentResize={syncNotePositions}
|
||||
headerActions={
|
||||
<NotesSendMenu
|
||||
worktreeId={worktreeId}
|
||||
groupId={worktreeId}
|
||||
modeIdParts={['markdown-notes', worktreeId, filePath, 'note', comment.id]}
|
||||
scopes={[
|
||||
{
|
||||
id: 'note',
|
||||
label: 'This note',
|
||||
notes: comment.sentAt ? [] : [comment as MarkdownReviewNote],
|
||||
prompt: formatMarkdownReviewNotes(
|
||||
[comment as MarkdownReviewNote],
|
||||
markdownReviewContent
|
||||
)
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="rich-markdown-review-note-action"
|
||||
title={
|
||||
copiedReviewNoteId === comment.id
|
||||
? 'Copied note'
|
||||
: 'Copy note for agent'
|
||||
}
|
||||
]}
|
||||
targetModeLabel="This note"
|
||||
triggerClassName="rich-markdown-review-note-send"
|
||||
disabledTooltip="Note already sent"
|
||||
onDelivered={(notes) =>
|
||||
void clearDeliveredDiffComments(worktreeId, notes)
|
||||
}
|
||||
/>
|
||||
aria-label={
|
||||
copiedReviewNoteId === comment.id
|
||||
? 'Copied note'
|
||||
: 'Copy note for agent'
|
||||
}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void handleCopyMarkdownReviewNote(comment as MarkdownReviewNote)
|
||||
}}
|
||||
>
|
||||
{copiedReviewNoteId === comment.id ? (
|
||||
<Check className="size-3.5" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<NotesSendMenu
|
||||
worktreeId={worktreeId}
|
||||
groupId={worktreeId}
|
||||
modeIdParts={[
|
||||
'markdown-notes',
|
||||
worktreeId,
|
||||
filePath,
|
||||
'note',
|
||||
comment.id
|
||||
]}
|
||||
scopes={[
|
||||
{
|
||||
id: 'note',
|
||||
label: 'This note',
|
||||
notes: comment.sentAt ? [] : [comment as MarkdownReviewNote],
|
||||
prompt: formatMarkdownReviewNotes(
|
||||
[comment as MarkdownReviewNote],
|
||||
markdownReviewContent
|
||||
)
|
||||
}
|
||||
]}
|
||||
targetModeLabel="This note"
|
||||
triggerClassName="rich-markdown-review-note-action"
|
||||
disabledTooltip="Note already sent"
|
||||
onDelivered={(notes) =>
|
||||
void clearDeliveredDiffComments(worktreeId, notes)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -1928,12 +2033,21 @@ export default function RichMarkdownEditor({
|
||||
<MessageSquare className="size-3.5" />
|
||||
<span>{markdownComments.length}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rich-markdown-review-rail-action"
|
||||
title={reviewNotesCopied ? 'Copied notes' : 'Copy notes for agent'}
|
||||
aria-label={reviewNotesCopied ? 'Copied notes' : 'Copy notes for agent'}
|
||||
onClick={() => void handleCopyMarkdownReviewNotes()}
|
||||
>
|
||||
{reviewNotesCopied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
||||
</button>
|
||||
<NotesSendMenu
|
||||
worktreeId={worktreeId}
|
||||
groupId={worktreeId}
|
||||
modeIdParts={['markdown-notes', worktreeId, filePath, 'rail']}
|
||||
scopes={unsentMarkdownReviewScope}
|
||||
triggerClassName="rich-markdown-review-rail-send"
|
||||
triggerClassName="rich-markdown-review-rail-action"
|
||||
onDelivered={(notes) => void clearDeliveredDiffComments(worktreeId, notes)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { DiffComment } from '../../../shared/types'
|
||||
import {
|
||||
copyMarkdownReviewNotesForAgent,
|
||||
type MarkdownReviewNoteClipboardWriter
|
||||
} from './markdown-review-note-copy'
|
||||
import type { MarkdownReviewNote } from './markdown-review-notes'
|
||||
|
||||
function note(overrides: Partial<Omit<DiffComment, 'source'>> = {}): MarkdownReviewNote {
|
||||
return {
|
||||
id: 'n1',
|
||||
worktreeId: 'wt1',
|
||||
filePath: 'README.md',
|
||||
source: 'markdown',
|
||||
lineNumber: 2,
|
||||
body: 'needs detail',
|
||||
createdAt: 0,
|
||||
side: 'modified',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('copyMarkdownReviewNotesForAgent', () => {
|
||||
it('copies the same formatted markdown-review payload used by agent handoff', async () => {
|
||||
const writeClipboardText = vi.fn<MarkdownReviewNoteClipboardWriter>().mockResolvedValue()
|
||||
|
||||
const copied = await copyMarkdownReviewNotesForAgent({
|
||||
notes: [note({ selectedText: 'specific phrase', body: 'reword this' })],
|
||||
content: 'one\nspecific phrase in a longer line',
|
||||
writeClipboardText
|
||||
})
|
||||
|
||||
expect(copied).toBe(true)
|
||||
expect(writeClipboardText).toHaveBeenCalledOnce()
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(
|
||||
[
|
||||
'File: README.md',
|
||||
'Source: markdown',
|
||||
'Line 2',
|
||||
'Excerpt:',
|
||||
'> specific phrase',
|
||||
'User comment: "reword this"'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
it('does not write the clipboard when there are no notes', async () => {
|
||||
const writeClipboardText = vi.fn<MarkdownReviewNoteClipboardWriter>().mockResolvedValue()
|
||||
|
||||
const copied = await copyMarkdownReviewNotesForAgent({
|
||||
notes: [],
|
||||
content: 'one',
|
||||
writeClipboardText
|
||||
})
|
||||
|
||||
expect(copied).toBe(false)
|
||||
expect(writeClipboardText).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { formatMarkdownReviewNotes, type MarkdownReviewNote } from './markdown-review-notes'
|
||||
|
||||
export type MarkdownReviewNoteClipboardWriter = (text: string) => Promise<void>
|
||||
|
||||
export async function copyMarkdownReviewNotesForAgent({
|
||||
notes,
|
||||
content,
|
||||
writeClipboardText
|
||||
}: {
|
||||
notes: readonly MarkdownReviewNote[]
|
||||
content: string
|
||||
writeClipboardText: MarkdownReviewNoteClipboardWriter
|
||||
}): Promise<boolean> {
|
||||
if (notes.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
await writeClipboardText(formatMarkdownReviewNotes(notes, content))
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user