✨ feat(vscode): 重写注释留会话记录、能接着说,允许画原型图

每块的重写记录存进 .spx/comment-rewrites.json(和问答分开,改名、删文件跟着走),块下面同问答区:灰字历史、当前这一轮、输入框;输入框里的话接着这块最近一次重写的会话说,交回新正文照样写回,写回失败也记下原因。
重写跑在 claude --restricted 下:只放开读搜、OpenPencil 的 op、建 prototypes 目录、改草稿目录,写注释的插件单独加载;超时 15 分钟。
进行中的、出错被叫停留着的那一轮记在扩展端,切走标签页再切回来照样接上(问 AI 同一套)。
This commit is contained in:
cruldra committed 2026-10-12 13:45:37 +11:00
1 parent ae4e2160ef
commit c9f60f5b30
32 files changed
+1654 -860

No files matched your search

+49 -5
View File
@@ -1,4 +1,4 @@
import type { QaEntry, QaMap } from './qaMap'
import type { QaEntry, QaMap, RewriteEntry, RewriteMap } from './qaMap'
import { Buffer } from 'node:buffer'
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
@@ -6,7 +6,7 @@ import * as path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { detachedFiles, dropEntries, moveEntries, readPathMap, removeFiles, writePathMap } from '../pathMap'
import { writeQaImages } from './qaImages'
import { latestSession, QA_FILE, withEntry, withoutEntry } from './qaMap'
import { latestSession, QA_FILE, REWRITE_FILE, withEntry, withoutEntry } from './qaMap'
function entry(id: string, createdAt: string, question = `问 ${id}`): QaEntry {
return { id, question, answer: `答 ${id}`, createdAt, sessionId: `s-${id}` }
@@ -177,8 +177,8 @@ describe('问题带的图片', () => {
})
it('删一次问答,它的图片跟着删;别的问答的图片留着', async () => {
const a = { ...entry('a', '2026-09-25T10:00:00.000Z'), images: await writeQaImages(root, 'a', [png('a1'), png('a2')]) }
const b = { ...entry('b', '2026-09-26T10:00:00.000Z'), images: await writeQaImages(root, 'b', [png('b1')]) }
const a: QaEntry = { ...entry('a', '2026-09-25T10:00:00.000Z'), images: await writeQaImages(root, 'a', [png('a1'), png('a2')]) }
const b: QaEntry = { ...entry('b', '2026-09-26T10:00:00.000Z'), images: await writeQaImages(root, 'b', [png('b1')]) }
await update(map => withEntry(withEntry(map, 'a.py', IF, a), 'a.py', DISPLAY, b))
expect(await exists(imageFile('a-1.png'))).toBe(true)
await update(map => withoutEntry(map, 'a.py', IF, 'a'))
@@ -188,7 +188,7 @@ describe('问题带的图片', () => {
})
it('源文件删了、问答跟着删时图片也删;改名、挪位置按问答 id 起的名字不用动', async () => {
const a = { ...entry('a', '2026-09-25T10:00:00.000Z'), images: await writeQaImages(root, 'a', [png('a1')]) }
const a: QaEntry = { ...entry('a', '2026-09-25T10:00:00.000Z'), images: await writeQaImages(root, 'a', [png('a1')]) }
await update(map => withEntry(map, 'src/a.py', IF, a))
await update(map => moveEntries(map, 'src', 'lib'))
expect(await readPathMap(root, QA_FILE)).toEqual({ 'lib/a.py': [{ anchor: IF, entries: [a] }] })
@@ -197,3 +197,47 @@ describe('问题带的图片', () => {
expect(await exists(imageFile('a-1.png'))).toBe(false)
})
})
describe('重写注释的记录:另存一份,和问答分开', () => {
const rewriteFile = (): string => path.join(root, '.spx', 'comment-rewrites.json')
const exists = (file: string): Promise<boolean> => fsp.access(file).then(() => true, () => false)
function rewrite(id: string, createdAt: string, extra: Partial<RewriteEntry> = {}): RewriteEntry {
return { id, question: '/ecc:writing-code-comments a.py:3-8', body: `正文 ${id}`, createdAt, sessionId: `r-${id}`, ...extra }
}
it('存在 .spx/comment-rewrites.json,不进问答那份;正文、没写进文件的原因跟着这条读得回来,接着最近一次重写的会话', async () => {
const map: RewriteMap = withEntry(withEntry({}, 'a.py', IF, rewrite('a', '2026-09-25T10:00:00.000Z')), 'a.py', IF, rewrite('b', '2026-09-26T10:00:00.000Z', { error: '这段注释的定界符认不出来,没有改文件。' }))
await writePathMap(root, REWRITE_FILE, map)
expect(await exists(rewriteFile())).toBe(true)
expect(await exists(qaFile())).toBe(false)
expect(await readPathMap(root, REWRITE_FILE)).toEqual(map)
expect(await readPathMap(root, QA_FILE)).toEqual({})
expect(latestSession(map['a.py'], IF)).toBe('r-b')
})
it('模型没交回正文的那次只记原因;正文和原因都没有、正文是空的不认', async () => {
await fsp.mkdir(path.dirname(rewriteFile()), { recursive: true })
const silent = rewrite('s', '2026-09-25T10:00:00.000Z', { error: '模型没有按要求交回注释正文,没有改文件。' })
delete silent.body
await fsp.writeFile(rewriteFile(), JSON.stringify({ 'a.py': [{ anchor: IF, entries: [
silent,
{ ...rewrite('bare', '2026-09-25T11:00:00.000Z'), body: undefined },
rewrite('blank', '2026-09-25T12:00:00.000Z', { body: ' ' }),
] }] }), 'utf8')
expect(await readPathMap(root, REWRITE_FILE)).toEqual({ 'a.py': [{ anchor: IF, entries: [silent] }] })
})
it('带的图片和问答的放在同一个目录,删一条重写记录只删它自己的', async () => {
const png = { mediaType: 'image/png', base64: Buffer.from('r').toString('base64') }
const image = path.join(root, '.spx', 'code-qa', 'r-1.png')
const withImage = rewrite('r', '2026-09-25T10:00:00.000Z', { images: await writeQaImages(root, 'r', [png]) })
const before = withEntry({}, 'a.py', IF, withImage)
await writePathMap(root, REWRITE_FILE, before)
expect(await readPathMap(root, REWRITE_FILE)).toEqual(before)
const after = withoutEntry(before, 'a.py', IF, 'r')
await writePathMap(root, REWRITE_FILE, after)
await removeFiles(root, detachedFiles(REWRITE_FILE, before, after))
expect(await exists(image)).toBe(false)
})
})