✨ 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,7 @@
|
||||
import type { CustomTextEditorProvider, Disposable, DocumentSymbol, Event, TextDocument, TextDocumentShowOptions, WebviewPanel } from 'vscode'
|
||||
import type { ClaudeProfile } from '../../cc/profiles'
|
||||
import type { ClaudeImage } from '../../cc/spawnClaude'
|
||||
import type { IndexFind } from '../callTree/endpoint'
|
||||
import type { CommentEdit, RewriteOutcome, WriteResult } from '../commentRewrite'
|
||||
import type { CommentEdit, CommentJob, WriteResult } from '../commentRewrite'
|
||||
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, SourceRange } from '../contract'
|
||||
import type { DiffBase } from '../diff/diffBase'
|
||||
import type { OldDocument, SentDocument } from '../diff/previewDiff'
|
||||
@@ -18,20 +17,18 @@ import type { FoundLocation } from './definitions'
|
||||
import type { DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
|
||||
import type { TargetSpot } from './openTarget'
|
||||
import { realpath } from 'node:fs/promises'
|
||||
import { dirname, relative, sep } from 'node:path'
|
||||
import { commands, EndOfLine, Position, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { webviewHtml, webviewUiRoot } from '../../webview/html'
|
||||
import { DocumentCallees } from '../calleeKinds'
|
||||
import { loadBlockCalls } from '../callTree/query'
|
||||
import { CallTreeSession } from '../callTree/session'
|
||||
import { findBlock, rewriteBlockComment } from '../commentRewrite'
|
||||
import { commentFollowUp, commentJob, commentRewritePrompt, findBlock, writeCommentBody } from '../commentRewrite'
|
||||
import { PreviewDiff } from '../diff/previewDiff'
|
||||
import { blockReplacement } from '../editText'
|
||||
import { locate } from '../folderStore'
|
||||
import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols'
|
||||
import { DocumentNames, resolveNames } from '../nameResolution'
|
||||
import { askCommentBody, askFailure } from '../qa/claudeAsk'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
@@ -181,12 +178,6 @@ class Preview {
|
||||
private readonly definitions: DefinitionSources<FoundLocation>
|
||||
/** webview 报来的视口顶部那块的起始行 */
|
||||
visibleLine: number | undefined
|
||||
/** 正在重写的注释(块的记号)。同一块没写完不再开一次 */
|
||||
private readonly rewriting = new Set<string>()
|
||||
/** 预览关掉时停掉还在跑的重写 */
|
||||
private readonly rewriteAbort = new AbortController()
|
||||
/** 每段正在重写的注释各自的停止开关;预览关掉时 [rewriteAbort] 会把它们都停掉 */
|
||||
private readonly rewriteStops = new Map<string, AbortController>()
|
||||
/** 最近一次发出去的文档(版本、块树、源码):问 AI 时按块的记号在里面找那块代码,对比基准变了时拿它重新比 */
|
||||
private sent: SentDocument | undefined
|
||||
/** 能看调用树的块(函数、类、接口卡片)落在几棵调用树里。文档发出去之后再数,不挡住第一次预览 */
|
||||
@@ -314,6 +305,25 @@ class Preview {
|
||||
})
|
||||
this.postQa()
|
||||
}),
|
||||
owner.qa.onDidRewrite((event) => {
|
||||
if (event.owner !== this.subject().uri.toString() || !this.ready)
|
||||
return
|
||||
if (event.kind === 'update') {
|
||||
const { askId, question, text, activity } = event.ask
|
||||
this.post({ type: 'rewriteProgress', askId, question, text, ...(activity !== undefined && { activity }) })
|
||||
return
|
||||
}
|
||||
const { end, entryId } = event
|
||||
this.post({
|
||||
type: 'rewriteEnd',
|
||||
askId: event.ask.askId,
|
||||
status: end.status,
|
||||
...(end.status !== 'stopped' && end.error !== undefined && { error: end.error }),
|
||||
...(end.status === 'done' && end.body !== undefined && { text: end.body }),
|
||||
...(entryId !== undefined && { entryId }),
|
||||
})
|
||||
this.postQa()
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -474,7 +484,6 @@ class Preview {
|
||||
|
||||
dispose(): void {
|
||||
this.callTrees.dispose()
|
||||
this.rewriteAbort.abort()
|
||||
clearTimeout(this.timer)
|
||||
this.names.dispose()
|
||||
this.lookups.dispose()
|
||||
@@ -588,13 +597,22 @@ class Preview {
|
||||
await this.filter.setOnlyChanges(msg.on)
|
||||
return
|
||||
case 'edit':
|
||||
await this.applyEdit(msg.startLine, msg.endLine, msg.text)
|
||||
await replaceDocumentLines(this.document, msg.startLine, msg.endLine, msg.text)
|
||||
return
|
||||
case 'rewriteComment':
|
||||
await this.rewriteComment(msg.anchor)
|
||||
await this.rewrite(msg.askId, msg.anchor, msg.request, msg.images)
|
||||
return
|
||||
case 'stopRewrite':
|
||||
this.rewriteStops.get(msg.anchor)?.abort()
|
||||
this.owner.qa.stopRewrite(msg.askId)
|
||||
return
|
||||
case 'deleteRewrite':
|
||||
await this.owner.qa.deleteRewrite(this.subject().uri, msg.anchor, msg.entryId)
|
||||
return
|
||||
case 'forgetRound':
|
||||
if (msg.kind === 'ask')
|
||||
this.owner.qa.forgetAsk(this.subject().uri, msg.anchor)
|
||||
else
|
||||
this.owner.qa.forgetRewrite(this.subject().uri, msg.anchor)
|
||||
return
|
||||
case 'selectTranslator':
|
||||
await this.registry.selectTranslator(this.languageId, msg.id)
|
||||
@@ -970,132 +988,90 @@ class Preview {
|
||||
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }) })
|
||||
}
|
||||
|
||||
/** 用写注释的技能重写这一段,只把正文套回原来的定界符,再走块编辑那条写回。 */
|
||||
private async rewriteComment(anchor: string): Promise<void> {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private failRewrite(message: string): void {
|
||||
void window.showErrorMessage(`代码阅读:${message}`)
|
||||
}
|
||||
|
||||
private async rewriteCommentNow(anchor: string, signal: AbortSignal): Promise<RewriteOutcome> {
|
||||
const failed = (message: string): RewriteOutcome => {
|
||||
this.failRewrite(message)
|
||||
return { status: 'failed', error: message }
|
||||
}
|
||||
const block = this.sent && findBlock(this.sent.blocks, anchor)
|
||||
if (!block)
|
||||
return failed('这块代码刚改过,找不到了。等预览刷新后再重写。')
|
||||
const target = block.commentTarget
|
||||
if (!target)
|
||||
return failed('这块不能写注释。')
|
||||
/**
|
||||
* 重写 anchor 那块的注释:按最近一次发出去的块树找到它,记下它现在的样子(写回前比对),把发给模型的话和写回的办法交给 CodeQa。
|
||||
* 点按钮(没有 request)发技能命令、开新会话;输入框里说的话接着这块最近一次重写的会话说,带上块现在在哪。
|
||||
* 找不到这块、文件有没保存的修改(模型读的是磁盘上的文件)、开不了时,直接回一条失败。
|
||||
*/
|
||||
private async rewrite(askId: string, anchor: string, request: string | undefined, images: ClaudeImage[]): Promise<void> {
|
||||
const fail = (error: string): void => this.post({ type: 'rewriteEnd', askId, status: 'failed', error })
|
||||
const { document } = this
|
||||
if (document.isDirty)
|
||||
return failed('文件还有没保存的修改,模型读的是磁盘上的文件:先保存再重写注释。')
|
||||
const block = this.sent && findBlock(this.sent.blocks, anchor)
|
||||
const job = block?.commentTarget && commentJob(block.commentTarget, block.startLine, block.endLine, line => document.lineAt(line).text, () => document.lineCount)
|
||||
if (!block || !job) {
|
||||
fail(block && !block.commentTarget ? '这块不能写注释。' : '这块代码刚改过,找不到了。等预览刷新后再重写。')
|
||||
return
|
||||
}
|
||||
const at = locate(document.uri)
|
||||
const cwd = at?.folder.uri.fsPath ?? dirname(document.uri.fsPath)
|
||||
let profile: ClaudeProfile | undefined
|
||||
try {
|
||||
profile = await this.owner.qa.profiles.current()
|
||||
if (!at) {
|
||||
fail('这个文件不在工作区文件夹里,重写记录没地方存。')
|
||||
return
|
||||
}
|
||||
catch (err) {
|
||||
return failed(err instanceof Error ? err.message : String(err))
|
||||
if (document.isDirty) {
|
||||
fail('文件还有没保存的修改,模型读的是磁盘上的文件:先保存再重写注释。')
|
||||
return
|
||||
}
|
||||
if (signal.aborted)
|
||||
return { status: 'stopped' }
|
||||
let question = ''
|
||||
let text = ''
|
||||
let activity: string | undefined
|
||||
const publish = (): void => {
|
||||
this.post({ type: 'rewriteProgress', anchor, question, text, ...(activity !== undefined && { activity }) })
|
||||
const prompt = commentRewritePrompt(at.key, block.startLine, block.endLine, request)
|
||||
const question = request ?? prompt
|
||||
const refused = await this.owner.qa.rewrite(document.uri, {
|
||||
askId,
|
||||
anchor,
|
||||
question,
|
||||
prompt,
|
||||
...(request !== undefined && { followUp: commentFollowUp(at.key, block.startLine, block.endLine, request) }),
|
||||
images,
|
||||
writeBack: body => this.writeBack(job, body),
|
||||
})
|
||||
if (refused !== undefined) {
|
||||
fail(refused)
|
||||
return
|
||||
}
|
||||
const outcome = await rewriteBlockComment({
|
||||
target,
|
||||
startLine: block.startLine,
|
||||
endLine: block.endLine,
|
||||
path: relative(cwd, document.uri.fsPath).split(sep).join('/'),
|
||||
}, {
|
||||
// 页面点按钮时还不知道那句技能命令,先报上去
|
||||
this.post({ type: 'rewriteProgress', askId, question, text: '' })
|
||||
this.postQa()
|
||||
}
|
||||
|
||||
/** 把交回的正文写回这份文件;预览关了也照样按 uri 打开文档来写。 */
|
||||
private async writeBack(job: CommentJob, body: string | undefined): Promise<string | undefined> {
|
||||
const document = await workspace.openTextDocument(this.document.uri)
|
||||
return writeCommentBody(job, body, {
|
||||
lineAt: line => document.lineAt(line).text,
|
||||
lineCount: () => document.lineCount,
|
||||
ask: (prompt) => {
|
||||
question = prompt
|
||||
publish()
|
||||
return askCommentBody({
|
||||
prompt,
|
||||
...(profile && { profilePath: profile.path }),
|
||||
cwd,
|
||||
signal,
|
||||
onEvent: (event) => {
|
||||
if (event.kind === 'text') {
|
||||
text = event.text
|
||||
activity = undefined
|
||||
}
|
||||
else {
|
||||
activity = event.label
|
||||
}
|
||||
publish()
|
||||
},
|
||||
})
|
||||
},
|
||||
failure: askFailure,
|
||||
write: edit => this.applyCommentEdit(edit),
|
||||
signal,
|
||||
write: edit => writeCommentEdit(document, edit),
|
||||
})
|
||||
if (outcome.status === 'failed')
|
||||
this.failRewrite(outcome.error)
|
||||
return outcome
|
||||
}
|
||||
|
||||
/** 写回重写好的注释:换掉原来那几行(同块编辑),或在块上面插进新写的几行。 */
|
||||
private async applyCommentEdit(edit: CommentEdit): Promise<WriteResult> {
|
||||
if (edit.kind === 'replace')
|
||||
return this.applyEdit(edit.startLine, edit.endLine, edit.text)
|
||||
const { document } = this
|
||||
if (edit.line >= document.lineCount)
|
||||
return 'missing'
|
||||
const eol = document.eol === EndOfLine.CRLF ? '\r\n' : '\n'
|
||||
const change = new WorkspaceEdit()
|
||||
change.insert(document.uri, new Position(edit.line, 0), edit.text.split('\n').join(eol) + eol)
|
||||
if (!await workspace.applyEdit(change)) {
|
||||
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
|
||||
return 'rejected'
|
||||
}
|
||||
return 'written'
|
||||
}
|
||||
|
||||
/** 把这几行换成 [text](见 WriteResult)。 */
|
||||
private async applyEdit(startLine: number, endLine: number, text: string): Promise<WriteResult> {
|
||||
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 'missing'
|
||||
}
|
||||
if (replacement.unchanged)
|
||||
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)) {
|
||||
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
|
||||
return 'rejected'
|
||||
}
|
||||
return 'written'
|
||||
}
|
||||
}
|
||||
|
||||
/** 写回重写好的注释:换掉原来那几行(同块编辑),或在块上面插进新写的几行。 */
|
||||
async function writeCommentEdit(document: TextDocument, edit: CommentEdit): Promise<WriteResult> {
|
||||
if (edit.kind === 'replace')
|
||||
return replaceDocumentLines(document, edit.startLine, edit.endLine, edit.text)
|
||||
if (edit.line >= document.lineCount)
|
||||
return 'missing'
|
||||
const eol = document.eol === EndOfLine.CRLF ? '\r\n' : '\n'
|
||||
const change = new WorkspaceEdit()
|
||||
change.insert(document.uri, new Position(edit.line, 0), edit.text.split('\n').join(eol) + eol)
|
||||
if (!await workspace.applyEdit(change)) {
|
||||
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
|
||||
return 'rejected'
|
||||
}
|
||||
return 'written'
|
||||
}
|
||||
|
||||
/** 把这几行换成 [text](见 WriteResult):块编辑和重写注释都走这条。 */
|
||||
async function replaceDocumentLines(document: TextDocument, startLine: number, endLine: number, text: string): Promise<WriteResult> {
|
||||
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 'missing'
|
||||
}
|
||||
if (replacement.unchanged)
|
||||
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)) {
|
||||
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
|
||||
return 'rejected'
|
||||
}
|
||||
return 'written'
|
||||
}
|
||||
Reference in new issue
Block a user