✨ 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

+43 -14
View File
@@ -1,7 +1,7 @@
import type { QaAction, QaState } from './qa'
import type { QaMessage, QaThreadView } from './types'
import { describe, expect, it } from 'vitest'
import { answerPreview, asksOf, EMPTY_QA, historyOf, PATH_ASK_ANCHOR, qaReducer, relativeTime } from './qa'
import { answerPreview, asksOf, chatReducer, EMPTY_CHAT, EMPTY_QA, historyOf, PATH_ASK_ANCHOR, qaReducer, relativeTime } from './qa'
const IF = 'function get_user > if @3fa1c2d0'
@@ -9,12 +9,12 @@ function thread(anchor: string, ...ids: string[]): QaThreadView {
return { anchor, entries: ids.map(id => ({ id, question: `问 ${id}`, answer: `答 ${id}`, createdAt: '2026-09-25T10:00:00.000Z' })) }
}
function message(threads: QaThreadView[], running: QaMessage['running'] = []): QaMessage {
return { type: 'qa', available: true, threads, path: [], subject: 'file', running }
function message(threads: QaThreadView[], current: QaMessage['current'] = []): QaMessage {
return { type: 'qa', available: true, threads, path: [], subject: 'file', current, rewrites: [], rewriteCurrent: [] }
}
function qa(threads: QaThreadView[], running: QaMessage['running'] = []): QaAction {
return { type: 'qa', message: message(threads, running) }
function qa(threads: QaThreadView[], current: QaMessage['current'] = []): QaAction {
return { type: 'qa', message: message(threads, current) }
}
function run(...actions: QaAction[]): QaState {
@@ -28,7 +28,7 @@ describe('问 AI 的状态', () => {
expect(asksOf(state, IF)).toEqual([{ askId: 'a1', anchor: IF, question: '为什么?', images: [], text: '', status: 'running' }])
state = qaReducer(state, { type: 'answer', askId: 'a1', text: '因为', activity: '读取 a.py' })
expect(asksOf(state, IF)[0]).toMatchObject({ text: '因为', activity: '读取 a.py' })
state = qaReducer(state, { type: 'answerEnd', end: { type: 'answerEnd', askId: 'a1', status: 'done', text: '因为要鉴权。', entryId: 'new' } })
state = qaReducer(state, { type: 'answerEnd', end: { askId: 'a1', status: 'done', text: '因为要鉴权。', entryId: 'new' } })
state = qaReducer(state, qa([thread(IF, 'old', 'new')]))
expect(asksOf(state, IF)).toEqual([{ askId: 'a1', anchor: IF, question: '为什么?', images: [], text: '因为要鉴权。', status: 'done', entryId: 'new' }])
expect(historyOf(state, IF).map(entry => entry.id)).toEqual(['old'])
@@ -51,7 +51,9 @@ describe('问 AI 的状态', () => {
threads: [],
path: [{ id: 'p', question: '这文件夹干什么', answer: '放源码', createdAt: '2026-09-25T10:00:00.000Z' }],
subject: 'folder',
running: [],
current: [],
rewrites: [],
rewriteCurrent: [],
},
})
expect(state.path.map(entry => entry.id)).toEqual(['p'])
@@ -64,7 +66,7 @@ describe('问 AI 的状态', () => {
let state = run(
qa([thread(IF, 'e1')]),
{ type: 'ask', askId: 'a1', anchor: IF, question: '1', images: [] },
{ type: 'answerEnd', end: { type: 'answerEnd', askId: 'a1', status: 'done', entryId: 'e1' } },
{ type: 'answerEnd', end: { askId: 'a1', status: 'done', entryId: 'e1' } },
{ type: 'ask', askId: 'a2', anchor: IF, question: '2', images: [] },
{ type: 'close', anchor: IF },
)
@@ -76,20 +78,20 @@ describe('问 AI 的状态', () => {
})
it('页面重新加载后:扩展端还在答的接上、展开它那块;已知道的不重复', () => {
const running = [{ askId: 'a1', anchor: IF, question: '1', text: '答了一半' }]
const running = [{ askId: 'a1', anchor: IF, question: '1', text: '答了一半', status: 'running' as const }]
let state = run(qa([], running))
expect(state.open).toEqual([IF])
expect(asksOf(state, IF)).toEqual([{ ...running[0], status: 'running' }])
expect(asksOf(state, IF)).toEqual([running[0]])
state = qaReducer(state, qa([], running))
expect(state.asks).toHaveLength(1)
})
it('停止、失败照记;不认识的问题的消息不理', () => {
let state = run(qa([]), { type: 'ask', askId: 'a1', anchor: IF, question: '1', images: [] }, { type: 'answer', askId: 'a1', text: '一半' })
state = qaReducer(state, { type: 'answerEnd', end: { type: 'answerEnd', askId: 'a1', status: 'stopped' } })
state = qaReducer(state, { type: 'answerEnd', end: { askId: 'a1', status: 'stopped' } })
expect(asksOf(state, IF)[0]).toMatchObject({ status: 'stopped', text: '一半' })
expect(qaReducer(state, { type: 'answer', askId: 'a1', text: '晚到的' })).toEqual(state)
expect(qaReducer(state, { type: 'answerEnd', end: { type: 'answerEnd', askId: 'nope', status: 'done' } })).toEqual(state)
expect(qaReducer(state, { type: 'answerEnd', end: { askId: 'nope', status: 'done' } })).toEqual(state)
})
it('问题带的图片跟着这一问:答完、失败、停止都还在;页面重新加载后接上的没有', () => {
@@ -97,12 +99,39 @@ describe('问 AI 的状态', () => {
let state = run(qa([]), { type: 'ask', askId: 'a1', anchor: IF, question: '看图', images })
expect(asksOf(state, IF)[0].images).toEqual(images)
for (const status of ['done', 'failed', 'stopped'] as const)
expect(asksOf(qaReducer(state, { type: 'answerEnd', end: { type: 'answerEnd', askId: 'a1', status } }), IF)[0].images).toEqual(images)
state = run(qa([], [{ askId: 'a2', anchor: IF, question: '1', text: '' }]))
expect(asksOf(qaReducer(state, { type: 'answerEnd', end: { askId: 'a1', status } }), IF)[0].images).toEqual(images)
state = run(qa([], [{ askId: 'a2', anchor: IF, question: '1', text: '', status: 'running' }]))
expect(asksOf(state, IF)[0].images).toBeUndefined()
})
})
describe('重写注释的状态:和问 AI 同一套', () => {
const SKILL = '/ecc:writing-code-comments a.py:3-8'
it('页面重新加载后:扩展端还在写的那一轮接上、展开它那块,后面的进展接得上;出错、被叫停留着的照原样画出来', () => {
let state = chatReducer(EMPTY_CHAT, { type: 'sync', available: true, threads: [], current: [
{ askId: 'r1', anchor: IF, question: SKILL, text: '一半', status: 'running' },
{ askId: 'r0', anchor: 'class A', question: '短一点', text: '', status: 'failed', error: '额度用完了' },
] })
expect(state.open).toEqual([IF, 'class A'])
expect(asksOf(state, IF)).toEqual([{ askId: 'r1', anchor: IF, question: SKILL, text: '一半', status: 'running' }])
expect(asksOf(state, 'class A')).toEqual([{ askId: 'r0', anchor: 'class A', question: '短一点', text: '', status: 'failed', error: '额度用完了' }])
state = chatReducer(state, { type: 'answer', askId: 'r1', question: SKILL, text: '写完了大半', activity: '运行 op export' })
expect(asksOf(state, IF)[0]).toMatchObject({ text: '写完了大半', activity: '运行 op export', status: 'running' })
state = chatReducer(state, { type: 'answerEnd', end: { askId: 'r1', status: 'done', text: '正文', error: '编辑器没接受这次修改,没有写回。', entryId: 'e1' } })
expect(asksOf(state, IF)[0]).toMatchObject({ status: 'done', text: '正文', error: '编辑器没接受这次修改,没有写回。', entryId: 'e1' })
state = chatReducer(state, { type: 'sync', available: true, threads: [], current: [{ askId: 'r0', anchor: 'class A', question: '短一点', text: '', status: 'failed', error: '额度用完了' }] })
expect(state.asks).toHaveLength(2)
})
it('点按钮那一轮开始时还不知道那句技能命令,扩展端开始后报上来就换上', () => {
let state = chatReducer(EMPTY_CHAT, { type: 'ask', askId: 'r1', anchor: IF, question: '', images: [] })
expect(state.open).toEqual([IF])
state = chatReducer(state, { type: 'answer', askId: 'r1', question: SKILL, text: '' })
expect(asksOf(state, IF)[0]).toMatchObject({ question: SKILL, status: 'running' })
})
})
describe('问的时间', () => {
const now = new Date(2026, 8, 26, 15, 0).getTime()
const at = (year: number, month: number, day: number, hour: number, minute: number, second = 0): string =>