✨ feat(vscode): 新增「了解一块代码」:AI 找功能代码占一栏,对话里接着改
主栏标题栏的放大镜问要了解哪一块,占一个栏位建一栏(和收藏共用 30 个栏位,视图改名 slot1..30,按 slotKindN 分收藏 / 功能挂标题栏按钮),同时开对话页让 Claude 只读地在全部工作区文件夹里找,按 --json-schema 交回环节 → 文件 + 符号 + 一句话。 对话复用预览顶部的聊天(CodeQa.askTop:会话续接、进行中的一轮、记录存 workspaceState);结构不合格算这一轮没答成、留着能再画出来。 条目每次画、点时按眼下的代码找:有语言插件的解析成代码块按「类名.方法名」找,没有的问 VS Code 文档符号;找不到画成灰的。
This commit is contained in:
1 parent
16d27004d8
commit
880bf7fc57
38 files changed
+3386
-378
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import type { ExtensionContext, Memento, Uri } from 'vscode'
|
||||
import type { AskRunner, CommentAnswer, CommentAsk } from './claudeAsk'
|
||||
import type { RewriteEvent, RewriteRequest } from './codeQa'
|
||||
import type { AskEvent, RewriteEvent, RewriteRequest } from './codeQa'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
@@ -45,6 +45,7 @@ vi.mock('../folderStore', () => ({
|
||||
}))
|
||||
vi.mock('../editor/pathPreview', () => ({
|
||||
pathQaLocation: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'a.ts' }),
|
||||
topQaLocation: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'file:///workspace\na.ts' }),
|
||||
}))
|
||||
vi.mock('./pathAskDescribe', () => ({ describePath: async () => ({ kind: 'file', relativePath: 'a.ts', head: { unavailable: '测试' } }) }))
|
||||
vi.mock('../../cc/profiles', () => ({
|
||||
@@ -262,3 +263,79 @@ describe('重写注释的记录', () => {
|
||||
expect(writeBack).toHaveBeenCalledWith('第二段。')
|
||||
})
|
||||
})
|
||||
|
||||
describe('顶部聊天要结构化结果的一轮(了解一块代码)', () => {
|
||||
/** 记在内存里的 workspaceState:顶部问答记在这里 */
|
||||
function memento(): Pick<Memento, 'get' | 'update'> {
|
||||
const values = new Map<string, unknown>()
|
||||
return { get: <T>(key: string) => values.get(key) as T, update: async (key: string, value: unknown) => void values.set(key, value) }
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const runner = vi.fn<AskRunner>()
|
||||
const qa = new CodeQa(context(), memento(), runner)
|
||||
services.push(qa)
|
||||
return { qa, runner }
|
||||
}
|
||||
|
||||
/** 等这一轮结束的事件 */
|
||||
function ended(qa: CodeQa, askId: string): Promise<Extract<AskEvent, { kind: 'end' }>> {
|
||||
return new Promise((resolve) => {
|
||||
qa.onDidAnswer((event) => {
|
||||
if (event.kind === 'end' && event.ask.askId === askId)
|
||||
resolve(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const run = { prompt: '完整的话', followUp: '接着说的话', schema: '{"type":"object"}', addDirs: ['/ws/server'] }
|
||||
const historyOf = (qa: CodeQa) => qa.view(uri, file => file.fsPath, 'feature').path
|
||||
|
||||
it('带上 schema 和加进来的目录;答完交给 settle,记下它给的回答;下一轮接着最近一次的会话说', async () => {
|
||||
const { qa, runner } = setup()
|
||||
const output = { groups: [] }
|
||||
runner.mockResolvedValueOnce({ text: '找到了。', sessionId: 's1', output })
|
||||
const settle = vi.fn(async () => ({ text: '整理好的回答' }))
|
||||
const first = ended(qa, '1')
|
||||
expect(await qa.askTop(uri, { askId: '1', question: '用户登录', images: [] }, run, settle)).toBeUndefined()
|
||||
expect(runner.mock.calls[0][0]).toMatchObject({ prompt: '完整的话', schema: '{"type":"object"}', addDirs: ['/ws/server'], cwd: '/workspace' })
|
||||
expect(runner.mock.calls[0][0]).not.toHaveProperty('followUp')
|
||||
expect((await first).end).toEqual({ status: 'done', answer: { text: '整理好的回答', sessionId: 's1', output } })
|
||||
expect(settle).toHaveBeenCalledWith({ text: '找到了。', sessionId: 's1', output })
|
||||
expect(historyOf(qa)).toEqual([expect.objectContaining({ question: '用户登录', answer: '整理好的回答' })])
|
||||
|
||||
runner.mockResolvedValueOnce({ text: '', sessionId: 's1', output })
|
||||
const second = ended(qa, '2')
|
||||
await qa.askTop(uri, { askId: '2', question: '还漏了续费失败重试', images: [] }, run, settle)
|
||||
await second
|
||||
expect(runner.mock.calls[1][0].followUp).toEqual({ sessionId: 's1', prompt: '接着说的话' })
|
||||
expect(historyOf(qa).map(entry => entry.question)).toEqual(['用户登录', '还漏了续费失败重试'])
|
||||
})
|
||||
|
||||
it('settle 说结构不合格:这一轮算失败、说清原因,不记', async () => {
|
||||
const { qa, runner } = setup()
|
||||
runner.mockResolvedValueOnce({ text: '只说了话', sessionId: 's1' })
|
||||
const failed = ended(qa, '1')
|
||||
await qa.askTop(uri, { askId: '1', question: '用户登录', images: [] }, run, async () => ({ error: 'AI 没按要求的结构交回结果(只回了一段话)。' }))
|
||||
expect((await failed).end).toEqual({ status: 'failed', error: 'AI 没按要求的结构交回结果(只回了一段话)。' })
|
||||
expect(historyOf(qa)).toEqual([])
|
||||
// 没答成的这一轮留着,页面重新加载后照样画出来
|
||||
await vi.waitFor(() => expect(qa.view(uri, file => file.fsPath, 'feature').current).toMatchObject([{ askId: '1', status: 'failed', error: 'AI 没按要求的结构交回结果(只回了一段话)。' }]))
|
||||
})
|
||||
|
||||
it('删掉这一页的全部问答:记下的没了,正在问的叫停', async () => {
|
||||
const { qa, runner } = setup()
|
||||
runner.mockResolvedValueOnce({ text: '', sessionId: 's1', output: {} })
|
||||
const first = ended(qa, '1')
|
||||
await qa.askTop(uri, { askId: '1', question: '用户登录', images: [] }, run, async () => ({ text: '回答' }))
|
||||
await first
|
||||
runner.mockImplementationOnce(ask => new Promise((_resolve, reject) => {
|
||||
ask.signal.addEventListener('abort', () => reject(new ClaudeCancelledError('')))
|
||||
}))
|
||||
const stopped = ended(qa, '2')
|
||||
await qa.askTop(uri, { askId: '2', question: '接着找', images: [] }, run, async () => ({ text: '回答' }))
|
||||
await qa.deleteTop(uri)
|
||||
expect((await stopped).end).toEqual({ status: 'stopped' })
|
||||
expect(historyOf(qa)).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user