✨ 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,6 +1,6 @@
|
||||
/**
|
||||
* 「重写注释」:用写注释的技能给一块代码重写(没有就新写)讲它的普通注释。模型只读文件、按 JSON Schema 交回注释正文,
|
||||
* 这里套回原来的定界符和缩进(没有注释时用语言的行注释记号),由扩展写回文件。
|
||||
* 「重写注释」:用写注释的技能给一块代码重写它的注释(没有就新写一段)。模型按 JSON Schema 交回注释正文,
|
||||
* 这里套回原来的定界符和缩进(没有注释时用语言给的空壳),由扩展写回文件。
|
||||
* 认 Python 的三引号、井号行注释,以及块注释和双斜杠行注释。认不出就不改。
|
||||
*/
|
||||
|
||||
@@ -8,8 +8,11 @@ import type { CommentTarget } from './blockView'
|
||||
import { describeToolUse } from '../cc/claudeProgress'
|
||||
import { commonIndent } from './editText'
|
||||
|
||||
/** 写注释的技能在哪个 Claude Code 插件里 */
|
||||
export const COMMENT_PLUGIN = 'ecc'
|
||||
|
||||
/** 跟在对话框里输入这个命令一样,由 Claude Code 自己加载写注释的技能。 */
|
||||
export const COMMENT_SKILL = '/ecc:writing-code-comments'
|
||||
export const COMMENT_SKILL = `/${COMMENT_PLUGIN}:writing-code-comments`
|
||||
|
||||
/** 模型交回的结构(`--json-schema`):body 是注释正文,不带注释符号。 */
|
||||
export const COMMENT_SCHEMA = JSON.stringify({
|
||||
@@ -224,19 +227,30 @@ export function refillComment(comment: string, body: string): string | undefined
|
||||
return refillDoc(comment, text) ?? refillLine(comment, text) ?? refillBlock(comment, text)
|
||||
}
|
||||
|
||||
/** 发给模型的整句话:技能命令后面跟代码块在哪,`路径:起-止`(路径相对 claude 的工作目录,行号 1 起,连同讲它的注释),别的不说。startLine、endLine 0 起。 */
|
||||
export function commentRewritePrompt(path: string, startLine: number, endLine: number): string {
|
||||
return `${COMMENT_SKILL} ${path}:${startLine + 1}-${endLine + 1}`
|
||||
/** 代码块在哪:`路径:起-止`(路径相对 claude 的工作目录,行号 1 起,连同讲它的注释)。startLine、endLine 0 起。 */
|
||||
function commentRange(path: string, startLine: number, endLine: number): string {
|
||||
return `${path}:${startLine + 1}-${endLine + 1}`
|
||||
}
|
||||
|
||||
/** 开新会话时发给模型的:技能命令后面跟代码块在哪;用户说了话(request)就空一行跟在后面。 */
|
||||
export function commentRewritePrompt(path: string, startLine: number, endLine: number, request?: string): string {
|
||||
const command = `${COMMENT_SKILL} ${commentRange(path, startLine, endLine)}`
|
||||
return request ? `${command}\n\n${request}` : command
|
||||
}
|
||||
|
||||
/** 接着这块的重写会话说话时发的:块现在在哪(写回注释后行号会变),空一行跟着用户的话。 */
|
||||
export function commentFollowUp(path: string, startLine: number, endLine: number, request: string): string {
|
||||
return `${commentRange(path, startLine, endLine)}\n\n${request}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 claude 的 stream-json 输出里读出来的:text 是注释正文到现在为止写出来的(结构化结果逐字长出来),activity 是正在做的事(「读取 routes.py」),
|
||||
* result 是答完了(body 是结构化结果里的正文,没按 COMMENT_SCHEMA 交回、正文是空的时没有),error 是 Claude Code 自己报的失败。
|
||||
* result 是答完了(body 是结构化结果里的正文,没按 COMMENT_SCHEMA 交回、正文是空的时没有;sessionId 是这次的会话),error 是 Claude Code 自己报的失败。
|
||||
*/
|
||||
export type CommentEvent
|
||||
= | { kind: 'text', text: string }
|
||||
| { kind: 'activity', label: string }
|
||||
| { kind: 'result', body?: string }
|
||||
| { kind: 'result', body?: string, sessionId: string }
|
||||
| { kind: 'error', message: string }
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -276,6 +290,8 @@ export function createCommentParser(): (line: string) => CommentEvent[] {
|
||||
/** 正在写的结构化结果(工具输入的 JSON 原文);不在写时没有 */
|
||||
let json: string | undefined
|
||||
let text = ''
|
||||
/** 开头 init 行报的会话 id;result 行没带时用它 */
|
||||
let sessionId = ''
|
||||
const grow = (body: string | undefined): CommentEvent[] => {
|
||||
if (body === undefined || body === text)
|
||||
return []
|
||||
@@ -312,6 +328,10 @@ export function createCommentParser(): (line: string) => CommentEvent[] {
|
||||
// 子代理的消息(parent_tool_use_id 不空)不是这次的结果
|
||||
if (!isRecord(line) || (line.parent_tool_use_id !== undefined && line.parent_tool_use_id !== null))
|
||||
return []
|
||||
if (line.type === 'system' && line.subtype === 'init') {
|
||||
sessionId = str(line.session_id)
|
||||
return []
|
||||
}
|
||||
if (line.type === 'stream_event')
|
||||
return isRecord(line.event) ? streamEvent(line.event) : []
|
||||
if (line.type === 'assistant')
|
||||
@@ -323,7 +343,7 @@ export function createCommentParser(): (line: string) => CommentEvent[] {
|
||||
return [{ kind: 'error', message: str(line.result).trim() || errors || str(line.subtype) || '没说原因' }]
|
||||
}
|
||||
const body = bodyOf(line.structured_output)
|
||||
return [{ kind: 'result', ...(body !== undefined && { body }) }]
|
||||
return [{ kind: 'result', ...(body !== undefined && { body }), sessionId: str(line.session_id) || sessionId }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,62 +392,49 @@ function splitSameLine(line: string, atLine: number, breakAt: number, doc: strin
|
||||
/** 写回文件的结果:written 写进去了,unchanged 和文件里一样不用写,missing 行号已经不在,rejected 编辑器没接受。 */
|
||||
export type WriteResult = 'written' | 'unchanged' | 'missing' | 'rejected'
|
||||
|
||||
/** 一次重写的结果:done 已经写进文件,failed 没写并带给人看的原因,stopped 是被叫停、没写。 */
|
||||
export type RewriteOutcome
|
||||
= | { status: 'done' }
|
||||
| { status: 'stopped' }
|
||||
| { status: 'failed', error: string }
|
||||
|
||||
/** 给哪块代码重写注释:「重写注释」写到哪,块的起止行(0 起,连同讲它的注释),文件相对 claude 工作目录的路径(`/` 分隔),语言的行注释记号。 */
|
||||
export interface CommentRewriteJob {
|
||||
/** 开始重写时记下的这块:「重写注释」写到哪,块的起止行(0 起,连同讲它的注释),这几行当时的原文(写回前比对,变了就不写)。 */
|
||||
export interface CommentJob {
|
||||
target: CommentTarget
|
||||
startLine: number
|
||||
endLine: number
|
||||
path: string
|
||||
snapshot: string
|
||||
}
|
||||
|
||||
/** 重写时要用的:读文件现在的行,问模型(拿到结构化结果里的正文,没有正文时 undefined;失败时抛错),出错时给人看的话,写回文件。 */
|
||||
export interface CommentRewriteDeps {
|
||||
/** 读这几行当时的原文,记成一次重写;行号已经不在文件里时没有。 */
|
||||
export function commentJob(target: CommentTarget, startLine: number, endLine: number, lineAt: (line: number) => string, lineCount: () => number): CommentJob | undefined {
|
||||
const snapshot = rangeText(startLine, endLine, lineAt, lineCount)
|
||||
return snapshot === undefined ? undefined : { target, startLine, endLine, snapshot }
|
||||
}
|
||||
|
||||
function rangeText(startLine: number, endLine: number, lineAt: (line: number) => string, lineCount: () => number): string | undefined {
|
||||
if (startLine < 0 || endLine < startLine || endLine >= lineCount())
|
||||
return undefined
|
||||
return Array.from({ length: endLine - startLine + 1 }, (_, i) => lineAt(startLine + i)).join('\n')
|
||||
}
|
||||
|
||||
/** 写回时要用的:读文件现在的行,写回文件。 */
|
||||
export interface CommentWriteDeps {
|
||||
lineAt: (line: number) => string
|
||||
lineCount: () => number
|
||||
ask: (prompt: string) => Promise<string | undefined>
|
||||
failure: (err: unknown) => string
|
||||
write: (edit: CommentEdit) => Promise<WriteResult>
|
||||
signal: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写一块代码的注释:问模型(提示只有技能命令和代码块在哪,见 commentRewritePrompt),拿结构化结果里的正文套回去写进文件。
|
||||
* 没交回结构化的正文、这块代码在重写期间变了、写不回去都算失败,文件不动。
|
||||
* 把模型交回的正文套回这块的注释写进文件:写进去了(或和文件里一样)返回 undefined;没写成返回给人看的原因,文件不动。
|
||||
* 没交回正文、这块代码在重写期间变了、定界符认不出、编辑器没接受,都不写。
|
||||
*/
|
||||
export async function rewriteBlockComment(job: CommentRewriteJob, deps: CommentRewriteDeps): Promise<RewriteOutcome> {
|
||||
const failed = (error: string): RewriteOutcome => ({ status: 'failed', error })
|
||||
const rangeText = (): string | undefined => job.endLine < deps.lineCount()
|
||||
? Array.from({ length: job.endLine - job.startLine + 1 }, (_, i) => deps.lineAt(job.startLine + i)).join('\n')
|
||||
: undefined
|
||||
const snapshot = job.startLine >= 0 && job.endLine >= job.startLine ? rangeText() : undefined
|
||||
if (snapshot === undefined)
|
||||
return failed('这块代码的位置已经变了,没有写回。')
|
||||
let body: string | undefined
|
||||
try {
|
||||
body = await deps.ask(commentRewritePrompt(job.path, job.startLine, job.endLine))
|
||||
}
|
||||
catch (err) {
|
||||
return deps.signal.aborted ? { status: 'stopped' } : failed(deps.failure(err))
|
||||
}
|
||||
if (deps.signal.aborted)
|
||||
return { status: 'stopped' }
|
||||
export async function writeCommentBody(job: CommentJob, body: string | undefined, deps: CommentWriteDeps): Promise<string | undefined> {
|
||||
if (body === undefined)
|
||||
return failed('模型没有按要求交回注释正文,没有改文件。')
|
||||
if (rangeText() !== snapshot)
|
||||
return failed('这块代码在重写期间变了,没有写回。')
|
||||
return '模型没有按要求交回注释正文,没有改文件。'
|
||||
if (rangeText(job.startLine, job.endLine, deps.lineAt, deps.lineCount) !== job.snapshot)
|
||||
return '这块代码在重写期间变了,没有写回。'
|
||||
const edit = commentEdit(job.target, deps.lineAt, body)
|
||||
if (!edit)
|
||||
return failed('这段注释的定界符认不出来,没有改文件。')
|
||||
return '这段注释的定界符认不出来,没有改文件。'
|
||||
const wrote = await deps.write(edit)
|
||||
if (wrote === 'written' || wrote === 'unchanged')
|
||||
return { status: 'done' }
|
||||
return failed(wrote === 'rejected' ? '编辑器没接受这次修改,没有写回。' : '这块代码在重写期间变了,没有写回。')
|
||||
return undefined
|
||||
return wrote === 'rejected' ? '编辑器没接受这次修改,没有写回。' : '这块代码在重写期间变了,没有写回。'
|
||||
}
|
||||
|
||||
/** 按记号在块树里找到这块。 */
|
||||
|
||||
Reference in new issue
Block a user