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
}