✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下

This commit is contained in:
cruldra committed 2026-09-26 21:12:47 +08:00
1 parent badc722640
commit fb53bb727c
34 files changed
+2726 -43

No files matched your search

@@ -0,0 +1,86 @@
import type { AskThreadProps } from './AskThread'
import type { Ask } from './qa'
import type { QaEntryView } from './types'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { AskThread, OrphanThreads } from './AskThread'
const NOW = new Date(2026, 8, 26, 15, 0).getTime()
const HISTORY: QaEntryView[] = [
{ id: 'e1', question: '为什么普通用户不能看别人的资料?', answer: '这是**权限控制**:不是管理员、又不是查自己时抛 `Forbidden`。', createdAt: new Date(2026, 8, 23, 10, 0).toISOString() },
{ id: 'e2', question: 'UserRole 还有哪些角色?', answer: '只有 ADMIN 和 USER。', createdAt: new Date(2026, 8, 25, 10, 0).toISOString() },
]
function render(props: Partial<AskThreadProps>): string {
return renderToStaticMarkup(<AskThread history={[]} asks={[]} open={false} now={NOW} onAsk={() => {}} onStop={() => {}} onDelete={() => {}} onClose={() => {}} {...props} />)
}
const text = (html: string): string => html.replace(/<[^>]*>/g, '')
function ask(overrides: Partial<Ask>): Ask {
return { askId: 'a1', anchor: 'x', question: 'Forbidden 抛出去以后前端收到什么?', text: '', status: 'running', ...overrides }
}
describe('块下面的问答区', () => {
it('没问过、没展开:什么都不画', () => {
expect(render({})).toBe('')
})
it('问过、没展开:只露一行「问过 N 次」,点开才看历史', () => {
const html = render({ history: HISTORY })
expect(html).toContain('data-collapsed')
expect(text(html)).toBe('问过 2 次')
expect(html).toContain('aria-expanded="false"')
expect(html).not.toContain('<textarea')
})
it('展开:灰字的历史(问题、时间、「已理解,删除」、回答前两行),当前这一轮照常颜色,底下输入框', () => {
const html = render({ open: true, history: HISTORY, asks: [ask({ text: '`Forbidden` 定义在 **core**', activity: '读取 exceptions.py' })] })
expect(html.match(/data-history=""/g)).toHaveLength(2)
expect(text(html)).toContain('为什么普通用户不能看别人的资料?3 天前')
expect(text(html)).toContain('UserRole 还有哪些角色?昨天')
expect(html.match(/title="已理解,删除" aria-label="已理解,删除"/g)).toHaveLength(2)
expect(text(html)).toContain('这是权限控制:不是管理员、又不是查自己时抛 Forbidden。')
expect(html).toContain('<hr class="cr-qa-divider"/>')
expect(html).toContain('data-current=""')
expect(html).toContain('data-streaming=""><p><code>Forbidden</code> 定义在 <strong>core</strong></p>')
expect(text(html)).toContain('读取 exceptions.py')
expect(html).toContain('placeholder="继续问… Enter 发送,Esc 收起"')
expect(html).toContain('title="停止" aria-label="停止"')
expect(html).not.toContain('aria-label="发送"')
})
it('还没问过:输入框提示问这块代码,发送按钮在没字时不能点;答完的回答按 Markdown 排、不再闪光标', () => {
expect(render({ open: true })).toContain('placeholder="问这块代码… Enter 发送,Esc 收起"')
expect(render({ open: true })).toMatch(/aria-label="发送" disabled=""/)
const done = render({ open: true, asks: [ask({ status: 'done', text: '答完了' })] })
expect(done).toContain('<div class="cr-markdown cr-qa-answer"><p>答完了</p>')
expect(done).not.toContain('data-streaming')
expect(done).not.toContain('role="status"')
})
it('出错、停止写在这一问下面', () => {
const failed = render({ open: true, asks: [ask({ status: 'failed', error: 'Claude Code 没登录或登录过期了' })] })
expect(failed).toContain('role="alert"')
expect(text(failed)).toContain('Claude Code 没登录或登录过期了')
expect(text(render({ open: true, asks: [ask({ status: 'stopped', text: '一半' })] }))).toContain('已停止,这次没存。')
expect(text(render({ open: true, asks: [ask({})] }))).toContain('正在想…')
})
})
describe('找不到原来位置的问答', () => {
it('灰字一栏:写着原来在哪,问答照样能删', () => {
const html = renderToStaticMarkup(
<OrphanThreads
threads={[{ anchor: 'function display > if @1', where: [{ kind: 'function', name: 'display' }, { kind: 'if' }], entries: HISTORY }]}
kindName={kind => ({ function: '函数', if: '条件' })[kind] ?? kind}
now={NOW}
onDelete={() => {}}
/>,
)
expect(text(html)).toContain('代码改动后找不到原来位置的问答 · 2 条')
expect(text(html)).toContain('原来在:函数 display › 条件')
expect(html.match(/aria-label="已理解,删除"/g)).toHaveLength(2)
})
})