✨ feat(vscode): 重写注释留会话记录、能接着说,允许画原型图
每块的重写记录存进 .spx/comment-rewrites.json(和问答分开,改名、删文件跟着走),块下面同问答区:灰字历史、当前这一轮、输入框;输入框里的话接着这块最近一次重写的会话说,交回新正文照样写回,写回失败也记下原因。 重写跑在 claude --restricted 下:只放开读搜、OpenPencil 的 op、建 prototypes 目录、改草稿目录,写注释的插件单独加载;超时 15 分钟。 进行中的、出错被叫停留着的那一轮记在扩展端,切走标签页再切回来照样接上(问 AI 同一套)。
This commit is contained in:
1 parent
ae4e2160ef
commit
c9f60f5b30
32 files changed
+1654
-860
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import type { BlockView, CommentTarget } from './blockView'
|
||||
import type { CommentEdit, CommentRewriteDeps, CommentRewriteJob, WriteResult } from './commentRewrite'
|
||||
import type { CommentEdit, CommentJob, CommentWriteDeps, WriteResult } from './commentRewrite'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from './blockView'
|
||||
import { COMMENT_SKILL, commentEdit, commentRewritePrompt, commentSource, createCommentParser, findBlock, refillComment, rewriteBlockComment } from './commentRewrite'
|
||||
import { COMMENT_SKILL, commentEdit, commentFollowUp, commentJob, commentRewritePrompt, commentSource, createCommentParser, findBlock, refillComment, writeCommentBody } from './commentRewrite'
|
||||
import { commonIndent, replaceLines, splitLines } from './editText'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createRustAdapter } from './languages/rust'
|
||||
@@ -135,10 +135,15 @@ function structuredLines(body: string): string[] {
|
||||
const resultLine = (fields: Record<string, unknown>): string => line({ type: 'result', subtype: 'success', is_error: false, ...fields })
|
||||
|
||||
describe('重写注释的提示', () => {
|
||||
it('只有技能命令和代码块在哪:路径:起-止,行号 1 起', () => {
|
||||
it('点按钮时只有技能命令和代码块在哪:路径:起-止,行号 1 起', () => {
|
||||
expect(commentRewritePrompt('src/acquisition/repository.py', 12, 20)).toBe(`${COMMENT_SKILL} src/acquisition/repository.py:13-21`)
|
||||
expect(commentRewritePrompt('a.ts', 0, 0)).toBe('/ecc:writing-code-comments a.ts:1-1')
|
||||
})
|
||||
|
||||
it('用户说了话:开新会话时跟在技能命令后面;接着会话说时前面是块现在在哪(写回注释后行号会变)', () => {
|
||||
expect(commentRewritePrompt('a.ts', 2, 9, '加上原型图')).toBe('/ecc:writing-code-comments a.ts:3-10\n\n加上原型图')
|
||||
expect(commentFollowUp('a.ts', 2, 9, '加上原型图')).toBe('a.ts:3-10\n\n加上原型图')
|
||||
})
|
||||
})
|
||||
|
||||
describe('读模型交回的结构化结果', () => {
|
||||
@@ -147,33 +152,37 @@ describe('读模型交回的结构化结果', () => {
|
||||
const events = [
|
||||
line({ type: 'assistant', message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/w/src/a.py' } }] } }),
|
||||
...structuredLines('把 `a` 和 `b` 相加。'),
|
||||
resultLine({ result: '{"body":"把 `a` 和 `b` 相加。"}', structured_output: { body: '把 `a` 和 `b` 相加。' } }),
|
||||
resultLine({ result: '{"body":"把 `a` 和 `b` 相加。"}', structured_output: { body: '把 `a` 和 `b` 相加。' }, session_id: 's1' }),
|
||||
].flatMap(parse)
|
||||
expect(events[0]).toEqual({ kind: 'activity', label: '读取 a.py' })
|
||||
expect(events.filter(event => event.kind === 'text').map(event => 'text' in event && event.text)).toEqual(['把 `', '把 `a` 和 `b` 相加。'])
|
||||
expect(events.at(-1)).toEqual({ kind: 'result', body: '把 `a` 和 `b` 相加。' })
|
||||
expect(events.at(-1)).toEqual({ kind: 'result', body: '把 `a` 和 `b` 相加。', sessionId: 's1' })
|
||||
})
|
||||
|
||||
it('会话 id 以 result 行的为准,没有时用开头 init 行的', () => {
|
||||
const parse = createCommentParser()
|
||||
expect(parse(line({ type: 'system', subtype: 'init', session_id: 's0' }))).toEqual([])
|
||||
expect(parse(resultLine({ structured_output: { body: '正文' } }))).toEqual([{ kind: 'result', body: '正文', sessionId: 's0' }])
|
||||
})
|
||||
|
||||
it('模型只回了一段话、structured_output 是空的:结果里没有正文;Claude Code 报错时是失败', () => {
|
||||
expect(createCommentParser()(resultLine({ result: '我改好了:把 a 和 b 相加。' }))).toEqual([{ kind: 'result' }])
|
||||
expect(createCommentParser()(resultLine({ result: '', structured_output: { body: ' ' } }))).toEqual([{ kind: 'result' }])
|
||||
expect(createCommentParser()(resultLine({ result: '我改好了:把 a 和 b 相加。', session_id: 's1' }))).toEqual([{ kind: 'result', sessionId: 's1' }])
|
||||
expect(createCommentParser()(resultLine({ result: '', structured_output: { body: ' ' }, session_id: 's1' }))).toEqual([{ kind: 'result', sessionId: 's1' }])
|
||||
expect(createCommentParser()(line({ type: 'result', is_error: true, result: '额度用完了' }))).toEqual([{ kind: 'error', message: '额度用完了' }])
|
||||
})
|
||||
})
|
||||
|
||||
/** 内存里的一份文件:按行读,写回时同预览一样换掉几行(补回缩进)或在某行上面插几行。 */
|
||||
function memoryFile(text: string): { deps: (ask: CommentRewriteDeps['ask']) => CommentRewriteDeps, text: () => string, writes: CommentEdit[] } {
|
||||
function memoryFile(text: string): { deps: CommentWriteDeps, text: () => string, writes: CommentEdit[] } {
|
||||
let current = text
|
||||
const writes: CommentEdit[] = []
|
||||
const lines = (): string[] => splitLines(current)
|
||||
return {
|
||||
text: () => current,
|
||||
writes,
|
||||
deps: ask => ({
|
||||
deps: {
|
||||
lineAt: index => lines()[index],
|
||||
lineCount: () => lines().length,
|
||||
ask,
|
||||
failure: err => `没答成:${err instanceof Error ? err.message : String(err)}`,
|
||||
write: async (edit): Promise<WriteResult> => {
|
||||
writes.push(edit)
|
||||
if (edit.kind === 'replace') {
|
||||
@@ -185,8 +194,7 @@ function memoryFile(text: string): { deps: (ask: CommentRewriteDeps['ask']) => C
|
||||
}
|
||||
return 'written'
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,10 +209,15 @@ async function pythonBlock(source: string, line: number): Promise<BlockView> {
|
||||
return found
|
||||
}
|
||||
|
||||
function job(block: BlockView, path = 'src/a.py'): CommentRewriteJob {
|
||||
/** 开始重写时记下这块:写到哪、范围、这几行当时的原文。 */
|
||||
function job(block: BlockView, source: string): CommentJob {
|
||||
if (!block.commentTarget)
|
||||
throw new Error('这块不能写注释')
|
||||
return { target: block.commentTarget, startLine: block.startLine, endLine: block.endLine, path }
|
||||
const lines = splitLines(source)
|
||||
const started = commentJob(block.commentTarget, block.startLine, block.endLine, index => lines[index], () => lines.length)
|
||||
if (!started)
|
||||
throw new Error('块不在文件里')
|
||||
return started
|
||||
}
|
||||
|
||||
const SOURCE = [
|
||||
@@ -219,18 +232,12 @@ const SOURCE = [
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
describe('重写一块代码的注释', () => {
|
||||
it('块前面的注释(几段隔着空行的也算)换成模型交回的正文,套回 # 和缩进;提示只有技能命令和块的范围', async () => {
|
||||
describe('把交回的正文写回这块代码', () => {
|
||||
it('块前面的注释(几段隔着空行的也算)换成交回的正文,套回 # 和缩进', async () => {
|
||||
const repo = await pythonBlock(SOURCE, 3)
|
||||
expect(repo.commentTarget).toEqual({ kind: 'replace', startLine: 2, endLine: 4 })
|
||||
const file = memoryFile(SOURCE)
|
||||
const prompts: string[] = []
|
||||
const outcome = await rewriteBlockComment(job(repo), file.deps(async (prompt) => {
|
||||
prompts.push(prompt)
|
||||
return '仓库:按名字找文件。'
|
||||
}))
|
||||
expect(outcome).toEqual({ status: 'done' })
|
||||
expect(prompts).toEqual(['/ecc:writing-code-comments src/a.py:3-8'])
|
||||
expect(await writeCommentBody(job(repo, SOURCE), '仓库:按名字找文件。', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe(SOURCE.replace('# 旧的说明\n\n# 第二段', '# 仓库:按名字找文件。'))
|
||||
})
|
||||
|
||||
@@ -239,12 +246,7 @@ describe('重写一块代码的注释', () => {
|
||||
const find = await pythonBlock(source, 2)
|
||||
expect(find.commentTarget).toEqual({ kind: 'replace', startLine: 2, endLine: 2 })
|
||||
const file = memoryFile(source)
|
||||
const prompts: string[] = []
|
||||
expect(await rewriteBlockComment(job(find), file.deps(async (prompt) => {
|
||||
prompts.push(prompt)
|
||||
return '按名字找\n\n找不到时返回 1'
|
||||
}))).toEqual({ status: 'done' })
|
||||
expect(prompts).toEqual(['/ecc:writing-code-comments src/a.py:2-4'])
|
||||
expect(await writeCommentBody(job(find, source), '按名字找\n\n找不到时返回 1', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe('class Repo:\n def find(self):\n """按名字找\n\n 找不到时返回 1\n """\n return 1\n')
|
||||
})
|
||||
|
||||
@@ -253,7 +255,7 @@ describe('重写一块代码的注释', () => {
|
||||
const find = await pythonBlock(source, 1)
|
||||
expect(find.commentTarget).toEqual({ kind: 'insert', line: 1, shell: '"""_"""' })
|
||||
const file = memoryFile(source)
|
||||
expect(await rewriteBlockComment(job(find), file.deps(async () => '按名字找。'))).toEqual({ status: 'done' })
|
||||
expect(await writeCommentBody(job(find, source), '按名字找。', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe('def find(self):\n """按名字找。"""\n return 1\n')
|
||||
})
|
||||
|
||||
@@ -262,7 +264,7 @@ describe('重写一块代码的注释', () => {
|
||||
const find = await pythonBlock(source, 1)
|
||||
expect(find.commentTarget).toMatchObject({ kind: 'insert', line: 0, shell: '"""_"""' })
|
||||
const file = memoryFile(source)
|
||||
expect(await rewriteBlockComment(job(find), file.deps(async () => '按名字找。'))).toEqual({ status: 'done' })
|
||||
expect(await writeCommentBody(job(find, source), '按名字找。', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe('def find():\n """按名字找。"""\n return 1\n')
|
||||
})
|
||||
|
||||
@@ -271,7 +273,7 @@ describe('重写一块代码的注释', () => {
|
||||
const find = await pythonBlock(source, 1)
|
||||
expect(find.commentTarget).toEqual({ kind: 'replace', startLine: 0, endLine: 0 })
|
||||
const file = memoryFile(source)
|
||||
expect(await rewriteBlockComment(job(find), file.deps(async () => '按名字找。'))).toEqual({ status: 'done' })
|
||||
expect(await writeCommentBody(job(find, source), '按名字找。', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe('# 按名字找。\ndef find():\n return 1\n')
|
||||
})
|
||||
|
||||
@@ -280,7 +282,7 @@ describe('重写一块代码的注释', () => {
|
||||
const find = await pythonBlock(source, 1)
|
||||
expect(find.commentTarget).toEqual({ kind: 'replace', startLine: 2, endLine: 2 })
|
||||
const file = memoryFile(source)
|
||||
expect(await rewriteBlockComment(job(find), file.deps(async () => '新的说明。'))).toEqual({ status: 'done' })
|
||||
expect(await writeCommentBody(job(find, source), '新的说明。', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe('# 旧的说明\ndef find():\n """新的说明。"""\n return 1\n')
|
||||
})
|
||||
|
||||
@@ -289,47 +291,32 @@ describe('重写一块代码的注释', () => {
|
||||
const header = await pythonBlock(source, 1)
|
||||
expect(header).toMatchObject({ kind: 'comment', commentTarget: { kind: 'replace', startLine: 0, endLine: 0 } })
|
||||
const file = memoryFile(source)
|
||||
const prompts: string[] = []
|
||||
await rewriteBlockComment(job(header), file.deps(async (prompt) => {
|
||||
prompts.push(prompt)
|
||||
return '新的模块说明。'
|
||||
}))
|
||||
expect(prompts).toEqual(['/ecc:writing-code-comments src/a.py:1-1'])
|
||||
expect(await writeCommentBody(job(header, source), '新的模块说明。', file.deps)).toBeUndefined()
|
||||
expect(file.text()).toBe('"""新的模块说明。"""\n\nimport os\n')
|
||||
})
|
||||
|
||||
it('没交回结构化的正文、模型出错:失败,文件不动', async () => {
|
||||
it('没交回结构化的正文:不写,说原因', async () => {
|
||||
const repo = await pythonBlock(SOURCE, 3)
|
||||
const silent = memoryFile(SOURCE)
|
||||
expect(await rewriteBlockComment(job(repo), silent.deps(async () => undefined))).toEqual({ status: 'failed', error: '模型没有按要求交回注释正文,没有改文件。' })
|
||||
expect(await writeCommentBody(job(repo, SOURCE), undefined, silent.deps)).toBe('模型没有按要求交回注释正文,没有改文件。')
|
||||
expect(silent.writes).toEqual([])
|
||||
const broken = memoryFile(SOURCE)
|
||||
expect(await rewriteBlockComment(job(repo), broken.deps(async () => {
|
||||
throw new Error('额度用完了')
|
||||
}))).toEqual({ status: 'failed', error: '没答成:额度用完了' })
|
||||
expect(broken.writes).toEqual([])
|
||||
})
|
||||
|
||||
it('重写期间这块代码变了:失败,不写;被叫停:stopped,不写', async () => {
|
||||
it('重写期间这块代码变了、行号已经不在文件里:不写;编辑器没接受:说原因', async () => {
|
||||
const repo = await pythonBlock(SOURCE, 3)
|
||||
const file = memoryFile(SOURCE)
|
||||
let edited = SOURCE
|
||||
const deps = file.deps(async () => {
|
||||
edited = SOURCE.replace('return os.sep', 'return "/"')
|
||||
return '新说明'
|
||||
})
|
||||
const changing: CommentRewriteDeps = { ...deps, lineAt: index => splitLines(edited)[index] }
|
||||
expect(await rewriteBlockComment(job(repo), changing)).toEqual({ status: 'failed', error: '这块代码在重写期间变了,没有写回。' })
|
||||
const edited = splitLines(SOURCE.replace('return os.sep', 'return "/"'))
|
||||
expect(await writeCommentBody(job(repo, SOURCE), '新说明', { ...file.deps, lineAt: index => edited[index] })).toBe('这块代码在重写期间变了,没有写回。')
|
||||
expect(await writeCommentBody(job(repo, SOURCE), '新说明', { ...file.deps, lineCount: () => 3 })).toBe('这块代码在重写期间变了,没有写回。')
|
||||
expect(file.writes).toEqual([])
|
||||
expect(await writeCommentBody(job(repo, SOURCE), '新说明', { ...file.deps, write: async () => 'rejected' })).toBe('编辑器没接受这次修改,没有写回。')
|
||||
expect(commentJob({ kind: 'replace', startLine: 0, endLine: 0 }, 5, 9, () => '', () => 3)).toBeUndefined()
|
||||
})
|
||||
|
||||
const stop = new AbortController()
|
||||
const stopped = memoryFile(SOURCE)
|
||||
const result = await rewriteBlockComment(job(repo), { ...stopped.deps(async () => {
|
||||
stop.abort()
|
||||
return '新说明'
|
||||
}), signal: stop.signal })
|
||||
expect(result).toEqual({ status: 'stopped' })
|
||||
expect(stopped.writes).toEqual([])
|
||||
it('认不出定界符:不写,说原因', async () => {
|
||||
const file = memoryFile('x = 1\n')
|
||||
const started = commentJob({ kind: 'replace', startLine: 0, endLine: 0 }, 0, 0, () => 'x = 1', () => 2)
|
||||
expect(started && await writeCommentBody(started, '说明', file.deps)).toBe('这段注释的定界符认不出来,没有改文件。')
|
||||
})
|
||||
|
||||
it('typescript 已经有 JSDoc:换掉这段说明,不在声明上面再写 //', async () => {
|
||||
|
||||
Reference in new issue
Block a user