✨ feat(vscode): 代码阅读对话记录加「复制回答」按钮

历史每条在删除左边、答完的当前这一轮在头部右边,点了把回答原样的 Markdown(带仓库链接、不带问题)放进剪贴板,成功图标换成对勾、提示「已复制」,失败提示「复制失败」,两秒后复原;复制文字并给反馈抽成 useCopyText 一份,日志弹窗也改用它,复制失败不再静默吞掉;两个头部按钮共用 cr-qa-action 样式
This commit is contained in:
cruldra committed 2026-10-14 22:32:50 +11:00
1 parent e81c810ca8
commit 76707adb13
5 files changed
+166 -27

No files matched your search

+26 -4
View File
@@ -3,10 +3,12 @@ import type { ClipboardEvent, DragEvent, KeyboardEvent, ReactElement } from 'rea
import type { Ask } from './qa'
import type { PendingImage } from './qaImages'
import type { ProfileChoice, QaEntryView } from './types'
import { ChevronDown, ChevronRight, CircleStop, Hourglass, LoaderCircle, MessageSquareText, Search, SendHorizontal, Sparkles, Trash2, TriangleAlert, X } from 'lucide-react'
import type { CopyState } from '@/hooks/useCopyText'
import { Check, ChevronDown, ChevronRight, CircleStop, Copy, Hourglass, LoaderCircle, MessageSquareText, Search, SendHorizontal, Sparkles, Trash2, TriangleAlert, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { SelectMenu } from '@/components/ui/select-menu'
import { useCopyText } from '@/hooks/useCopyText'
import { renderMarkdown, renderMarkdownOnce } from './markdown'
import { answerPreview, relativeTime, unfinished } from './qa'
import { MAX_IMAGES, pickImages, readImage, transferFiles } from './qaImages'
@@ -109,8 +111,26 @@ function ImagePreview({ url, onClose }: { url: string, onClose: () => void }): R
)
}
/** 复制按钮在每种复制结果下的图标和名字。 */
const COPY_LOOKS: Record<CopyState, { icon: LucideIcon, label: string }> = {
idle: { icon: Copy, label: '复制回答' },
copied: { icon: Check, label: '已复制' },
failed: { icon: TriangleAlert, label: '复制失败' },
}
/** 头部右边的「复制回答」:把 `answer` 原样的 Markdown 连同里面的仓库链接放进剪贴板,好拿到别的会话里接着用;点完图标和名字换成复制的结果,两秒后复原。 */
function CopyAnswerButton({ answer }: { answer: string }): ReactElement {
const { state, copy } = useCopyText()
const { icon: Icon, label } = COPY_LOOKS[state]
return (
<button type="button" className="cr-tool cr-qa-action" data-failed={state === 'failed' ? '' : undefined} data-tooltip={label} aria-label={label} onClick={() => void copy(answer)}>
<Icon size={13} strokeWidth={1.8} aria-hidden />
</button>
)
}
/**
* 说过的一次,灰字:说的话、时间、删除(问答叫「已理解,删除」),带的图片,下面回答的前两行;点问题或预览展开整段回答(照样灰字,按 Markdown 排)。
* 说过的一次,灰字:说的话、时间、复制回答、删除(问答叫「已理解,删除」),带的图片,下面回答的前两行;点问题或预览展开整段回答(照样灰字,按 Markdown 排)。
* 重写记录没写进文件时,最下面写着原因
*/
function HistoryItem({ entry, wording, now, onDelete, onOpenImage }: { entry: QaEntryView, wording: ThreadWording, now: number, onDelete: () => void, onOpenImage: (url: string) => void }): ReactElement {
@@ -130,7 +150,8 @@ function HistoryItem({ entry, wording, now, onDelete, onOpenImage }: { entry: Qa
</time>
{entry.profile && <span data-tooltip="用这个 Claude Code 配置(profile)答的">{` · ${entry.profile}`}</span>}
</span>
<button type="button" className="cr-tool cr-qa-delete" data-tooltip={wording.deleteLabel} aria-label={wording.deleteLabel} onClick={onDelete}>
<CopyAnswerButton answer={entry.answer} />
<button type="button" className="cr-tool cr-qa-action" data-tooltip={wording.deleteLabel} aria-label={wording.deleteLabel} onClick={onDelete}>
<Trash2 size={13} strokeWidth={1.8} aria-hidden />
</button>
</div>
@@ -152,7 +173,7 @@ function HistoryItem({ entry, wording, now, onDelete, onOpenImage }: { entry: Qa
)
}
/** 当前这一轮说的一次,照常颜色:说的话,回答逐字长出来(按 Markdown 排),排着队、在做的事、出错(重写注释没写进文件也算)、停止写在下面 */
/** 当前这一轮说的一次,照常颜色:说的话(答完了右边能复制回答),回答逐字长出来(按 Markdown 排),排着队、在做的事、出错(重写注释没写进文件也算)、停止写在下面 */
function CurrentItem({ ask, wording, onOpenImage }: { ask: Ask, wording: ThreadWording, onOpenImage: (url: string) => void }): ReactElement {
const running = ask.status === 'running'
const label = ask.activity ?? (ask.text === '' ? '正在想…' : wording.answering)
@@ -162,6 +183,7 @@ function CurrentItem({ ask, wording, onOpenImage }: { ask: Ask, wording: ThreadW
<div className="cr-qa-head">
<Icon size={12} strokeWidth={1.8} className="cr-qa-icon" aria-hidden />
<span className="cr-qa-question">{ask.question}</span>
{!unfinished(ask) && ask.text !== '' && <CopyAnswerButton answer={ask.text} />}
</div>
<Thumbs urls={ask.images ?? []} onOpen={onOpenImage} />
{ask.text !== '' && (