✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下

This commit is contained in:
cruldra committed 2026-09-26 21:12:47 +08:00
1 parent badc722640
commit fb53bb727c
34 files changed
+2726 -43

No files matched your search

+68 -1
View File
@@ -1,5 +1,7 @@
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
import type { BlockView } from '../blockView'
import type { RuleLocation, RuleReference } from '../contract'
import type { CodeQa } from '../qa/codeQa'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
import type { BlockFilter } from './blockFilter'
@@ -9,6 +11,8 @@ import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
import { logger } from '../../logging/logger'
import { webviewHtml, webviewUiRoot } from '../../webview/html'
import { blockReplacement } from '../editText'
import { locate } from '../folderStore'
import { askPrompt, followUpPrompt } from '../qa/prompt'
import { definitionAction } from './definitionAction'
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
import { DefinitionSources } from './definitionSources'
@@ -36,6 +40,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
private readonly filter: BlockFilter,
/** 库规则标的引用去哪儿查定义(交给标它的规则) */
readonly resolveReference: (reference: RuleReference) => Promise<RuleLocation[]>,
/** 问 AI:各预览共用,同一份文件开在几处时问答、正在答的问题都一样 */
readonly qa: CodeQa,
) {}
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
@@ -98,6 +104,8 @@ class Preview {
private readonly definitions: DefinitionSources<FoundLocation>
/** webview 报来的视口顶部那块的起始行 */
visibleLine: number | undefined
/** 最近一次发出去的块树和它的源码:问 AI 时按块的记号在里面找那块代码 */
private sent: { blocks: BlockView[], source: string } | undefined
constructor(
private readonly document: TextDocument,
@@ -131,6 +139,28 @@ class Preview {
if (this.ready)
this.post({ type: 'filter', excluded })
}),
owner.qa.onDidChange((uris) => {
if (uris.some(uri => uri.toString() === document.uri.toString()))
this.postQa()
}),
owner.qa.onDidAnswer((event) => {
if (event.owner !== document.uri.toString() || !this.ready)
return
if (event.kind === 'update') {
const { askId, text, activity } = event.ask
this.post({ type: 'answer', askId, text, ...(activity !== undefined && { activity }) })
return
}
const { end, entryId } = event
this.post({
type: 'answerEnd',
askId: event.ask.askId,
status: end.status,
...(end.status === 'failed' && { error: end.error }),
...(end.status === 'done' && { text: end.answer.text, entryId, sessionId: end.answer.sessionId }),
})
this.postQa()
}),
]
}
@@ -151,12 +181,15 @@ class Preview {
if (!this.ready)
return
const request = ++this.latest
const message: ExtensionToWebview = await documentMessage(this.document, this.registry, this.runtime, (text) => {
const { languageId, version } = this.document
const source = this.document.getText()
const message = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
})
if (request !== this.latest || !this.ready)
return
this.definitions.remember(message.version, message.blocks)
this.sent = { blocks: message.blocks, source }
const reveal = this.pendingReveal
this.pendingReveal = undefined
this.post(reveal === undefined ? message : { ...message, reveal })
@@ -180,6 +213,30 @@ class Preview {
void this.panel.webview.postMessage(message)
}
private postQa(): void {
if (this.ready)
this.post(this.owner.qa.view(this.document.uri))
}
/**
* 问 anchor 那块代码:按最近一次发出去的块树找到它、带上代码上下文交给 CodeQa;resume 是追问时上一次的会话 id。
* 找不到这块(代码刚改过、还没重新发)或问不了时,直接回一条失败。
*/
private ask(askId: string, anchor: string, question: string, resume: string | undefined): void {
const at = locate(this.document.uri)
const subject = at && this.sent && { path: at.key, languageId: this.document.languageId, ...this.sent, anchor }
const prompt = subject && askPrompt(subject, question)
const followUp = subject && resume ? followUpPrompt(subject, question) : undefined
const refused = prompt === undefined
? '这块代码刚改过,找不到了。等预览刷新后重新点「问 AI」再问。'
: this.owner.qa.ask(this.document.uri, { askId, anchor, question, prompt, ...(resume && followUp && { followUp: { sessionId: resume, prompt: followUp } }) })
if (refused !== undefined) {
this.post({ type: 'answerEnd', askId, status: 'failed', error: refused })
return
}
this.postQa()
}
private schedulePush(): void {
clearTimeout(this.timer)
this.timer = setTimeout(() => void this.push(), REFRESH_DELAY_MS)
@@ -191,8 +248,18 @@ class Preview {
this.ready = true
// 先于文档发:第一份文档按筛选画,不会先整份露出来再藏
this.post({ type: 'filter', excluded: this.filter.excluded })
this.postQa()
await this.push()
return
case 'ask':
this.ask(msg.askId, msg.anchor, msg.question, msg.resume)
return
case 'stopAsk':
this.owner.qa.stop(msg.askId)
return
case 'deleteQa':
await this.owner.qa.delete(this.document.uri, msg.anchor, msg.entryId)
return
case 'setFilter':
await this.filter.set(msg.excluded)
return