From 2841e2b477aa57dcdb3f9d711e01be76cc5e0ca4 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 14 Oct 2026 23:16:20 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=98=85=E8=AF=BB=E5=AF=B9=E8=AF=9D=E9=87=8C=E7=9A=84?= =?UTF-8?q?=E5=9B=9E=E7=AD=94=E6=82=AC=E5=81=9C=E9=93=BE=E6=8E=A5=E6=97=B6?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E4=B8=80=E7=9B=B4=E9=97=AA=E3=80=81=E7=9B=96?= =?UTF-8?q?=E4=BD=8F=E9=93=BE=E6=8E=A5=EF=BC=9A=E5=9B=9E=E7=AD=94=E6=AF=8F?= =?UTF-8?q?=E6=AC=A1=E9=87=8D=E7=94=BB=E9=83=BD=E6=96=B0=E5=BB=BA=20{=5F?= =?UTF-8?q?=5Fhtml}=EF=BC=8CReact=20=E6=8D=A2=E4=BA=86=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=B0=B1=E6=95=B4=E6=AE=B5=E9=87=8D=E8=AE=BE=20innerHTML?= =?UTF-8?q?=EF=BC=8C=E5=BC=B9=E5=87=BA=E5=AE=9A=E4=B9=89=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=BC=95=E8=B5=B7=E7=9A=84=E9=87=8D=E7=94=BB=E6=8A=8A=E6=82=AC?= =?UTF-8?q?=E5=81=9C=E7=9D=80=E7=9A=84=E9=93=BE=E6=8E=A5=E6=8D=A2=E6=88=90?= =?UTF-8?q?=E6=96=B0=E5=85=83=E7=B4=A0=EF=BC=8C=E5=8D=A1=E7=89=87=E6=8C=89?= =?UTF-8?q?=E8=84=B1=E7=A6=BB=E9=A1=B5=E9=9D=A2=E7=9A=84=E6=97=A7=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E8=90=BD=E5=88=B0=E5=B7=A6=E4=B8=8A=E8=A7=92=E7=9B=96?= =?UTF-8?q?=E4=BD=8F=E9=93=BE=E6=8E=A5=EF=BC=8C=E6=8C=87=E9=92=88=E4=B8=8B?= =?UTF-8?q?=E7=9A=84=E6=96=B0=E9=93=BE=E6=8E=A5=E5=8F=88=E9=87=8D=E6=96=B0?= =?UTF-8?q?=E9=97=AE=E5=8D=A1=E7=89=87=E3=80=81=E5=86=8D=E8=A2=AB=E6=8D=A2?= =?UTF-8?q?=E6=8E=89=EF=BC=8C=E6=9D=A5=E5=9B=9E=E9=97=AA=EF=BC=9B=E5=9B=9E?= =?UTF-8?q?=E7=AD=94=E6=94=B9=E7=94=A8=20MarkdownPreview=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E6=AE=B5=20HTML=20=E7=94=A8=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E5=AF=B9=E8=B1=A1=EF=BC=8C=E9=93=BE=E6=8E=A5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=A2=AB=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/codeReader/AskThread.test.tsx | 53 +++++++++++++++++++ .../webview-ui/src/codeReader/AskThread.tsx | 11 ++-- .../webview-ui/src/codeReader/TextPreview.tsx | 5 +- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/vscode/webview-ui/src/codeReader/AskThread.test.tsx b/vscode/webview-ui/src/codeReader/AskThread.test.tsx index 42d3d4d..25ab9d4 100644 --- a/vscode/webview-ui/src/codeReader/AskThread.test.tsx +++ b/vscode/webview-ui/src/codeReader/AskThread.test.tsx @@ -237,3 +237,56 @@ describe('复制回答', () => { expect(writeText).toHaveBeenCalledExactlyOnceWith('找到了 **3 条**路径') }) }) + +describe('回答里的链接', () => { + let container: HTMLDivElement + let root: Root + + beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + container = document.createElement('div') + document.body.append(container) + root = createRoot(container) + }) + + afterEach(async () => { + await act(async () => root.unmount()) + container.remove() + vi.unstubAllGlobals() + }) + + const ANSWER = '- [HomeShell.startSessionWith](src/app/home/HomeShell.tsx#HomeShell.startSessionWith):把首句暂存后跳到编程聊天页' + + async function mount(props: Partial): Promise { + await act(async () => root.render( {}} onAsk={() => {}} onStop={() => {}} onDelete={() => {}} onClose={() => {}} {...props} />)) + } + + function link(): HTMLAnchorElement { + const found = container.querySelector('.cr-qa-answer a') + if (!found) + throw new Error('回答里没有链接') + return found + } + + it('展开的历史回答:页面为别的事重画(如弹出定义卡片)、传进等值的新对象时,还是原来那个链接,悬停的那处不被换掉', async () => { + const history = (): QaEntryView[] => [{ ...HISTORY[0]!, answer: ANSWER }] + await mount({ history: history() }) + await act(async () => container.querySelector('[data-history] .cr-qa-question')!.click()) + const original = link() + for (const now of [NOW + 1, NOW + 2]) + await mount({ history: history(), now }) + expect(link()).toBe(original) + expect(original.isConnected).toBe(true) + }) + + it('答完的当前一轮同样;回答的字变了才换成新的链接', async () => { + await mount({ open: true, asks: [ask({ status: 'done', text: ANSWER })] }) + const original = link() + await mount({ open: true, asks: [ask({ status: 'done', text: ANSWER })], now: NOW + 1 }) + expect(link()).toBe(original) + expect(original.isConnected).toBe(true) + await mount({ open: true, asks: [ask({ status: 'done', text: `${ANSWER}\n- 再往下` })] }) + expect(original.isConnected).toBe(false) + expect(link().textContent).toBe('HomeShell.startSessionWith') + }) +}) diff --git a/vscode/webview-ui/src/codeReader/AskThread.tsx b/vscode/webview-ui/src/codeReader/AskThread.tsx index e9b306c..40cb385 100644 --- a/vscode/webview-ui/src/codeReader/AskThread.tsx +++ b/vscode/webview-ui/src/codeReader/AskThread.tsx @@ -12,6 +12,7 @@ import { useCopyText } from '@/hooks/useCopyText' import { renderMarkdown, renderMarkdownOnce } from './markdown' import { answerPreview, relativeTime, unfinished } from './qa' import { MAX_IMAGES, pickImages, readImage, transferFiles } from './qaImages' +import { MarkdownPreview } from './TextPreview' import { showInViewport } from './useReveal' /** 一种对话在界面上怎么称呼:区域的名字、图标、「问过 N 次」、删一条、输入框的提示和名字、回答长出来时的状态 */ @@ -157,7 +158,7 @@ function HistoryItem({ entry, wording, now, onDelete, onOpenImage }: { entry: Qa {expanded - ?
+ ? : (
{ask.text !== '' && ( -
)} {running && ( diff --git a/vscode/webview-ui/src/codeReader/TextPreview.tsx b/vscode/webview-ui/src/codeReader/TextPreview.tsx index 89d8f20..0399dd7 100644 --- a/vscode/webview-ui/src/codeReader/TextPreview.tsx +++ b/vscode/webview-ui/src/codeReader/TextPreview.tsx @@ -25,7 +25,8 @@ export const TextPreview = memo(({ text, className, find }: { text: PreviewText, }) TextPreview.displayName = 'TextPreview' -export function MarkdownPreview({ html, className, find }: { html: string, className?: string, find?: string }): ReactElement { +export function MarkdownPreview({ html, className, find, streaming }: { html: string, className?: string, find?: string, streaming?: boolean }): ReactElement { + // 同一段 HTML 用同一个对象:React 见到新对象就整段重设 innerHTML,里面的链接都换成新元素,悬停着的那个就脱离了页面 const content = useMemo(() => ({ __html: html }), [html]) - return
+ return
}