✨ 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,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 =>
|
||||
|
||||
Reference in new issue
Block a user