✨ 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

+55 -37
View File
@@ -1,43 +1,42 @@
import type { RewriteRuns } from './rewriteRun'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { rewriteEnd, rewriteProgress, startRewrite } from './rewriteRun'
import type { RewriteTree } from './BlockTree'
import type { ChatState } from './qa'
import type { ProfileChoice } from './types'
import { useEffect, useMemo, useReducer, useRef } from 'react'
import { chatReducer, EMPTY_CHAT, newAskId } from './qa'
import { toAskImage } from './qaImages'
import { ANSWER_FRAME_MS } from './useQa'
import { onMessage, postMessage } from './vscode'
/** 按注释规范重写:过程只在这一页上,按块的记号记。不写入问答,刷新或关掉预览就没了。 */
export function useRewrite(): { runs: RewriteRuns, onRewrite: (anchor: string) => void, onStop: (anchor: string) => void } {
const [runs, setRuns] = useState<RewriteRuns>(() => new Map())
/** 动作里读眼下的过程(这块是不是已经在重写) */
const current = useRef(runs)
current.current = runs
/**
* 按注释规范重写:收扩展端的重写记录、当前一轮的进展,给块树用的状态和动作。和问 AI 同一套:
* 进行中的、出错被叫停留着的那一轮记在扩展端,页面重新加载后随 qa 消息的 rewriteCurrent 接上。profile 下拉和问 AI 共用。
*/
export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string) => void): RewriteTree {
const [state, dispatch] = useReducer(chatReducer<ChatState>, EMPTY_CHAT)
useEffect(() => {
/** 攒着没画的进展:anchor → 最新的一份 */
/** 攒着没画的进展:askId → 最新的一份 */
const pending = new Map<string, { question: string, text: string, activity?: string }>()
let timer: ReturnType<typeof setTimeout> | undefined
const flush = (): void => {
timer = undefined
setRuns((prev) => {
let next = prev
for (const [anchor, progress] of pending)
next = rewriteProgress(next, anchor, progress.question, progress.text, progress.activity)
return next
})
for (const [askId, progress] of pending)
dispatch({ type: 'answer', askId, ...progress })
pending.clear()
}
const off = onMessage((msg) => {
if (msg.type === 'rewriteProgress') {
pending.set(msg.anchor, { question: msg.question, text: msg.text, ...(msg.activity !== undefined && { activity: msg.activity }) })
timer ??= setTimeout(flush, ANSWER_FRAME_MS)
return
}
if (msg.type === 'rewriteEnd') {
pending.delete(msg.anchor)
if (pending.size === 0) {
switch (msg.type) {
case 'qa':
dispatch({ type: 'sync', available: msg.available, threads: msg.rewrites, current: msg.rewriteCurrent })
return
case 'rewriteProgress':
pending.set(msg.askId, { question: msg.question, text: msg.text, ...(msg.activity !== undefined && { activity: msg.activity }) })
timer ??= setTimeout(flush, ANSWER_FRAME_MS)
return
case 'rewriteEnd':
clearTimeout(timer)
timer = undefined
}
setRuns(prev => rewriteEnd(prev, msg.anchor, msg.status, msg.error))
flush()
dispatch({ type: 'answerEnd', end: msg })
}
})
return () => {
@@ -46,14 +45,33 @@ export function useRewrite(): { runs: RewriteRuns, onRewrite: (anchor: string) =
}
}, [])
const onRewrite = useCallback((anchor: string) => {
if (current.current.get(anchor)?.status === 'running')
return
setRuns(prev => startRewrite(prev, anchor))
postMessage({ type: 'rewriteComment', anchor })
}, [])
const onStop = useCallback((anchor: string) => {
postMessage({ type: 'stopRewrite', anchor })
}, [])
return useMemo(() => ({ runs, onRewrite, onStop }), [runs, onRewrite, onStop])
/** 动作里读眼下的状态(这块是不是已经在写) */
const current = useRef(state)
current.current = state
return useMemo((): RewriteTree => ({
state,
profiles,
onSelectProfile,
onRewrite: (anchor) => {
if (current.current.asks.some(ask => ask.anchor === anchor && ask.status === 'running'))
return
const askId = newAskId()
// 那句技能命令由扩展端拼好,开始后随 rewriteProgress 报上来
dispatch({ type: 'ask', askId, anchor, question: '', images: [] })
postMessage({ type: 'rewriteComment', askId, anchor, images: [] })
},
onAsk: (anchor, request, images) => {
const askId = newAskId()
dispatch({ type: 'ask', askId, anchor, question: request, images: images.map(image => image.url) })
postMessage({ type: 'rewriteComment', askId, anchor, request, images: images.map(toAskImage) })
},
onStop: askId => postMessage({ type: 'stopRewrite', askId }),
onDelete: (anchor, entryId) => postMessage({ type: 'deleteRewrite', anchor, entryId }),
onOpen: anchor => dispatch({ type: 'open', anchor }),
onClose: (anchor) => {
dispatch({ type: 'close', anchor })
postMessage({ type: 'forgetRound', kind: 'rewrite', anchor })
},
}), [state, profiles, onSelectProfile])
}