✨ feat(vscode): 重写注释时在卡片下展示模型过程

This commit is contained in:
cruldra committed 2026-10-03 03:22:20 +08:00
1 parent 08fb04e3fd
commit 5e8e7b65ee
15 files changed
+369 -83

No files matched your search

+7 -1
View File
@@ -89,6 +89,8 @@ export interface QaMessage {
* askProfiles:「问 AI」输入框旁边的 profile 下拉:选项和选中的那项;ready 后发一次,任何一个预览改了都发给所有预览
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
* answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停
* rewriteProgress:anchor 这段注释正在按规范重写。question 是发给模型的要求,text 是到现在为止的全文,activity 是正在读什么
* rewriteEnd:这段重写结束了。done 是正文已经写进文件;failed 带给人看的原因,文件没改;stopped 是被叫停,文件没改
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现,由这两条命令发给活动的预览
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
* diff:这份文件相比对比基准改了什么(侧栏开着「未提交的更改」或审查工单时才有,见 DiffBase):每发一份文档之后算好了补发,
@@ -106,7 +108,8 @@ export type ExtensionToWebview
| ({ type: 'askProfiles' } & ProfileChoice)
| { type: 'answer', askId: string, text: string, activity?: string }
| { type: 'answerEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string }
| { type: 'commentRewritten', anchor: string }
| { type: 'rewriteProgress', anchor: string, question: string, text: string, activity?: string }
| { type: 'rewriteEnd', anchor: string, status: 'done' | 'failed' | 'stopped', error?: string }
| { type: 'diff', diff: DocumentDiff | null }
/**
@@ -120,6 +123,8 @@ export type ExtensionToWebview
* setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览
* ask:问 anchor 那块代码;askId 由 webview 起,回答的消息带回去;扩展端接着这块最近一次问答的会话问,用眼下选的 profile;
* images 是贴进输入框的图片(没贴是空的),随问题发给 Claude Code,答成了跟这条一起存
* rewriteComment:按注释规范重写 anchor 这段注释。过程用 rewriteProgress / rewriteEnd 发回页面,不写入问答
* stopRewrite:叫停 anchor 这段正在进行的重写,不写文件
* stopAsk:叫停 askId 那个问题,这次不存
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
* selectAskProfile:「问 AI」换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后发 askProfiles 给所有预览
@@ -132,6 +137,7 @@ export type WebviewToExtension
| { type: 'setOnlyChanges', on: boolean }
| { type: 'edit', startLine: number, endLine: number, text: string }
| { type: 'rewriteComment', anchor: string }
| { type: 'stopRewrite', anchor: string }
| { type: 'selectTranslator', id: string }
| { type: 'visibleLine', line: number }
| { type: 'probeDefinition', id: number, version: number, offset: number }
+91 -46
View File
@@ -129,6 +129,12 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
}
}
/** 一段注释重写的结果:`done` 已经写进文件,`failed` 没写并带给人看的原因,`stopped` 是被叫停、没写。 */
type RewriteOutcome
= | { status: 'done' }
| { status: 'stopped' }
| { status: 'failed', error: string }
class Preview {
/** webview 发过 ready 之后才发消息;标签页切走时页面被销毁,切回来重新加载、再发 ready,这期间也不发 */
private ready = false
@@ -144,6 +150,8 @@ class Preview {
private readonly rewriting = new Set<string>()
/** 预览关掉时停掉还在跑的重写 */
private readonly rewriteAbort = new AbortController()
/** 每段正在重写的注释各自的停止开关;预览关掉时 [rewriteAbort] 会把它们都停掉 */
private readonly rewriteStops = new Map<string, AbortController>()
/** 最近一次发出去的文档(版本、块树、源码):问 AI 时按块的记号在里面找那块代码,对比基准变了时拿它重新比 */
private sent: SentDocument | undefined
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
@@ -464,6 +472,9 @@ class Preview {
case 'rewriteComment':
await this.rewriteComment(msg.anchor)
return
case 'stopRewrite':
this.rewriteStops.get(msg.anchor)?.abort()
return
case 'selectTranslator':
await this.registry.selectTranslator(this.languageId, msg.id)
this.owner.refresh(this.languageId)
@@ -555,12 +566,19 @@ class Preview {
if (this.rewriting.has(anchor) || this.rewriteAbort.signal.aborted)
return
this.rewriting.add(anchor)
const stop = new AbortController()
const onDispose = (): void => stop.abort()
this.rewriteAbort.signal.addEventListener('abort', onDispose)
this.rewriteStops.set(anchor, stop)
try {
await this.rewriteCommentNow(anchor)
const outcome = await this.rewriteCommentNow(anchor, stop.signal)
if (!this.rewriteAbort.signal.aborted)
this.post({ type: 'rewriteEnd', anchor, status: outcome.status, ...(outcome.status === 'failed' && { error: outcome.error }) })
}
finally {
this.rewriteAbort.signal.removeEventListener('abort', onDispose)
this.rewriteStops.delete(anchor)
this.rewriting.delete(anchor)
this.post({ type: 'commentRewritten', anchor })
}
}
@@ -568,18 +586,18 @@ class Preview {
void window.showErrorMessage(`代码阅读:${message}`)
}
private async rewriteCommentNow(anchor: string): Promise<void> {
const found = this.sent && findPlaced(this.sent.blocks, anchor)
if (!found || found.block.kind !== 'comment') {
this.failRewrite('这块注释刚改过,找不到了。等预览刷新后再重写。')
return
private async rewriteCommentNow(anchor: string, signal: AbortSignal): Promise<RewriteOutcome> {
const failed = (message: string): RewriteOutcome => {
this.failRewrite(message)
return { status: 'failed', error: message }
}
const found = this.sent && findPlaced(this.sent.blocks, anchor)
if (!found || found.block.kind !== 'comment')
return failed('这块注释刚改过,找不到了。等预览刷新后再重写。')
const { block, siblings, index } = found
const { document } = this
if (block.startLine < 0 || block.endLine < block.startLine || block.endLine >= document.lineCount) {
this.failRewrite('这块注释的位置已经变了,没有写回。')
return
}
if (block.startLine < 0 || block.endLine < block.startLine || block.endLine >= document.lineCount)
return failed('这块注释的位置已经变了,没有写回。')
const raw = Array.from({ length: block.endLine - block.startLine + 1 }, (_, i) => document.lineAt(block.startLine + i).text)
const snapshot = raw.join('\n')
const comment = commentSource(raw)
@@ -596,65 +614,92 @@ class Preview {
earlierKinds: siblings.slice(0, index).map(item => item.kind),
...(following !== undefined && { following }),
})
if (!prompt) {
this.failRewrite('这段注释的定界符认不出来,没有改文件。')
return
}
if (!prompt)
return failed('这段注释的定界符认不出来,没有改文件。')
const at = locate(document.uri)
let text = ''
let activity: string | undefined
const publish = (): void => {
this.post({
type: 'rewriteProgress',
anchor,
question: prompt,
text,
...(activity !== undefined && { activity }),
})
}
publish()
const profile = await this.owner.qa.profiles.current()
let text: string
if (signal.aborted)
return { status: 'stopped' }
let answerText: string
try {
const answer = await askClaude({
prompt,
images: [],
...(profile && { profilePath: profile.path }),
cwd: at?.folder.uri.fsPath ?? dirname(document.uri.fsPath),
signal: this.rewriteAbort.signal,
onEvent: () => {},
signal,
onEvent: (event) => {
if (event.kind === 'text') {
text = event.text
activity = undefined
}
else if (event.kind === 'activity') {
activity = event.label
}
else {
return
}
publish()
},
})
text = answer.text
answerText = answer.text
}
catch (err) {
if (!this.rewriteAbort.signal.aborted)
this.failRewrite(askFailure(err))
return
}
if (this.rewriteAbort.signal.aborted)
return
const body = modelCommentBody(text)
if (!body) {
this.failRewrite('模型没有给出注释正文,没有改文件。')
return
}
if (block.endLine >= document.lineCount) {
this.failRewrite('这段注释在重写期间变了,没有写回。')
return
if (signal.aborted)
return { status: 'stopped' }
return failed(askFailure(err))
}
if (signal.aborted)
return { status: 'stopped' }
const body = modelCommentBody(answerText)
if (!body)
return failed('模型没有给出注释正文,没有改文件。')
if (block.endLine >= document.lineCount)
return failed('这段注释在重写期间变了,没有写回。')
const current = Array.from({ length: block.endLine - block.startLine + 1 }, (_, i) => document.lineAt(block.startLine + i).text).join('\n')
if (current !== snapshot) {
this.failRewrite('这段注释在重写期间变了,没有写回。')
return
}
if (current !== snapshot)
return failed('这段注释在重写期间变了,没有写回。')
const filled = refillComment(comment, body)
if (!filled) {
this.failRewrite('这段注释的定界符认不出来,没有改文件。')
return
}
await this.applyEdit(block.startLine, block.endLine, filled)
if (!filled)
return failed('这段注释的定界符认不出来,没有改文件。')
if (signal.aborted)
return { status: 'stopped' }
const wrote = await this.applyEdit(block.startLine, block.endLine, filled)
if (wrote === 'written' || wrote === 'unchanged')
return { status: 'done' }
if (wrote === 'rejected')
return { status: 'failed', error: `没能把修改写回 ${workspace.asRelativePath(document.uri)}` }
return failed('这段注释在重写期间变了,没有写回。')
}
private async applyEdit(startLine: number, endLine: number, text: string): Promise<void> {
/** 把这几行换成 [text]。`written` 写进去了,`unchanged` 跟文件里一样不用写,`missing` 行号已经不在,`rejected` 编辑器没接受。 */
private async applyEdit(startLine: number, endLine: number, text: string): Promise<'written' | 'unchanged' | 'missing' | 'rejected'> {
const { document } = this
const replacement = blockReplacement(line => document.lineAt(line).text, document.lineCount, startLine, endLine, text)
if (!replacement) {
logger.add({ level: 'warn', source: 'codeReader', message: `块内修改的行号 ${startLine}-${endLine} 已不在 ${document.uri.fsPath} 里,没有写回` })
return
return 'missing'
}
if (replacement.unchanged)
return
return 'unchanged'
const edit = new WorkspaceEdit()
edit.replace(document.uri, new Range(replacement.startLine, 0, replacement.endLine, replacement.endCharacter), replacement.text)
if (!await workspace.applyEdit(edit))
if (!await workspace.applyEdit(edit)) {
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
return 'rejected'
}
return 'written'
}
}