✨ feat(vscode): 代码阅读预览里工作区外的库文件也能问 AI,问答记进第一个工作区文件夹,重写注释不对库文件开放

This commit is contained in:
cruldra committed 2026-10-13 21:50:54 +11:00
1 parent cecae0e244
commit 39dfd9b084
24 files changed
+463 -79

No files matched your search

+76 -9
View File
@@ -29,25 +29,46 @@ vi.mock('vscode', async () => {
}
})
/** 工作区只有 /workspace 一个文件夹;/workspace 下面的是项目里的文件,别处的是工作区外的库文件 */
const places = vi.hoisted(() => {
const folder = { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }
/** ⌘ 点进去的标准库源码所在的目录 */
const libraryDir = '/home/me/.local/share/uv/python/cpython-3.12/lib/python3.12'
const libraryKey = '~/.local/share/uv/python/cpython-3.12/lib/python3.12/pathlib.py'
const inWorkspace = (uri: { fsPath: string }): boolean => uri.fsPath.startsWith('/workspace/')
/** 同 locateQa:项目里的记在 /workspace、键是相对路径;库文件也记在 /workspace(第一个工作区文件夹),键是 ~/…,发给模型写绝对路径、放行它所在的目录 */
const locateQa = (uri: { fsPath: string }) => inWorkspace(uri)
? { folder, key: 'a.ts', promptPath: 'a.ts' }
: { folder, key: libraryKey, promptPath: uri.fsPath, outsideDir: libraryDir }
return { folder, libraryDir, libraryKey, inWorkspace, locateQa }
})
vi.mock('../folderStore', () => ({
/** 内存里的一份表:按键取、改了就换 */
/** 内存里的表,每个工作区文件夹一份:按键取、改了就换 */
FolderStore: class {
private map: Record<string, unknown> = {}
private readonly maps = new Map<unknown, Record<string, unknown>>()
readonly onDidChange = () => ({ dispose: () => {} })
get(_folder: unknown, key: string) { return this.map[key] }
async update(_folder: unknown, change: (map: Record<string, unknown>) => Record<string, unknown>) {
this.map = change(this.map)
get(folder: unknown, key: string) { return this.maps.get(folder)?.[key] }
async update(folder: unknown, change: (map: Record<string, unknown>) => Record<string, unknown>) {
this.maps.set(folder, change(this.maps.get(folder) ?? {}))
}
dispose() {}
},
locate: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'a.ts' }),
locate: (uri: { fsPath: string }) => places.inWorkspace(uri) ? { folder: places.folder, key: 'a.ts' } : undefined,
locateQa: places.locateQa,
}))
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' }),
pathQaLocation: places.locateQa,
topQaLocation: (uri: { fsPath: string }) => {
const at = places.locateQa(uri)
return { folder: at.folder, key: `file:///workspace\n${at.key}` }
},
}))
/** 材料只用位置给的路径,不读盘 */
vi.mock('./pathAskDescribe', () => ({
describePath: async ({ at }: { at: { promptPath: string, outsideDir?: string } }) => ({ kind: 'file', path: at.promptPath, ...(at.outsideDir && { outside: true }), head: { unavailable: '测试' } }),
}))
vi.mock('./pathAskDescribe', () => ({ describePath: async () => ({ kind: 'file', relativePath: 'a.ts', head: { unavailable: '测试' } }) }))
vi.mock('../../cc/profiles', () => ({
getDefaultProfilePath: () => '/p/offical.json',
getProfilesDir: () => '/p',
@@ -411,3 +432,49 @@ describe('顶部聊天要结构化结果的一轮(了解一块代码)', () =
expect(historyOf(qa)).toEqual([])
})
})
describe('工作区外的库文件(⌘ 点进去的标准库源码)', () => {
const library = { fsPath: `${places.libraryDir}/pathlib.py`, toString: () => `file://${places.libraryDir}/pathlib.py` } as Uri
const viewOf = (qa: CodeQa, of: Uri = library) => qa.view(of, file => file.fsPath, 'file')
/** 等这一轮问答结束的事件 */
function answered(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)
})
})
}
it('能问 AI,不能重写注释(会改到库文件本身);项目里的文件两样都能', () => {
const { qa } = service(context())
expect(viewOf(qa)).toMatchObject({ available: true, rewritable: false })
expect(viewOf(qa, uri)).toMatchObject({ available: true, rewritable: true })
})
it('问一块:claude 在第一个工作区文件夹里跑、另外放行读库文件所在的目录;答完记进那个文件夹的表,再看能看到这条', async () => {
const { qa, runner } = service(context())
runner.mockResolvedValueOnce({ text: '按通配符递归找。', sessionId: 's1' })
const done = answered(qa, '1')
expect(await qa.ask(library, { askId: '1', anchor: 'function rglob', question: '怎么找的', prompt: '完整的', followUp: '短的', images: [] })).toBeUndefined()
expect(runner.mock.calls[0][0]).toMatchObject({ cwd: '/workspace', addDirs: [places.libraryDir] })
expect((await done).end.status).toBe('done')
expect(viewOf(qa).threads).toEqual([{ anchor: 'function rglob', entries: [expect.objectContaining({ question: '怎么找的', answer: '按通配符递归找。' })] }])
expect(viewOf(qa, uri).threads).toEqual([])
})
it('顶部问这个文件:一样放行库文件所在的目录,发给模型的是绝对路径', async () => {
const { qa, runner } = service(context())
expect(await qa.askPath(library, 'file', { askId: '1', question: '这是什么', images: [] })).toBeUndefined()
expect(runner.mock.calls[0][0]).toMatchObject({ cwd: '/workspace', addDirs: [places.libraryDir] })
expect(runner.mock.calls[0][0].prompt).toContain(`绝对路径:${places.libraryDir}/pathlib.py`)
})
it('重写注释开不了,不启动模型', async () => {
const { qa, rewriter } = service(context())
const skill = '/ecc:writing-code-comments pathlib.py:1-3'
expect(await qa.rewrite(library, { askId: '1', anchor: 'function rglob', question: skill, prompt: skill, images: [], writeBack: async () => undefined })).toMatch(/不能重写注释/)
expect(rewriter).not.toHaveBeenCalled()
})
})