✨ feat(vscode): 重写注释排成一队,整个扩展一次只跑一段

OpenPencil 同一时间只能开一个画板,两段同时画会互相掐断。所有预览、所有文件的重写(第一轮和接着说)排成一队,前一段写成、出错、被叫停后按先后自动开始下一段;问 AI 照旧最多同时 3 个。
排着的那一轮记在扩展端(rewriteCurrent 的 queued),块下面写「排队中」、能取消排队,切标签页、页面重新加载后照样显示。
This commit is contained in:
cruldra committed 2026-10-12 15:11:22 +11:00
1 parent b7c29c60e1
commit aecc4d1ab5
15 files changed
+329 -114

No files matched your search

+19 -11
View File
@@ -3,12 +3,12 @@ import type { ClipboardEvent, DragEvent, KeyboardEvent, ReactElement } from 'rea
import type { Ask } from './qa'
import type { PendingImage } from './qaImages'
import type { ProfileChoice, QaEntryView } from './types'
import { ChevronDown, ChevronRight, CircleStop, LoaderCircle, MessageSquareText, SendHorizontal, Sparkles, Trash2, TriangleAlert, X } from 'lucide-react'
import { ChevronDown, ChevronRight, CircleStop, Hourglass, LoaderCircle, MessageSquareText, SendHorizontal, Sparkles, Trash2, TriangleAlert, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { SelectMenu } from '@/components/ui/select-menu'
import { renderMarkdown, renderMarkdownOnce } from './markdown'
import { answerPreview, relativeTime } from './qa'
import { answerPreview, relativeTime, unfinished } from './qa'
import { MAX_IMAGES, pickImages, readImage, transferFiles } from './qaImages'
/** 一种对话在界面上怎么称呼:区域的名字、图标、「问过 N 次」、删一条、输入框的提示和名字、回答长出来时的状态 */
@@ -140,7 +140,7 @@ function HistoryItem({ entry, wording, now, onDelete, onOpenImage }: { entry: Qa
)
}
/** 当前这一轮说的一次,照常颜色:说的话,回答逐字长出来(按 Markdown 排),在做的事、出错(重写注释没写进文件也算)、停止写在下面 */
/** 当前这一轮说的一次,照常颜色:说的话,回答逐字长出来(按 Markdown 排),排着队、在做的事、出错(重写注释没写进文件也算)、停止写在下面 */
function CurrentItem({ ask, wording, onOpenImage }: { ask: Ask, wording: ThreadWording, onOpenImage: (url: string) => void }): ReactElement {
const running = ask.status === 'running'
const label = ask.activity ?? (ask.text === '' ? '正在想…' : wording.answering)
@@ -165,7 +165,13 @@ function CurrentItem({ ask, wording, onOpenImage }: { ask: Ask, wording: ThreadW
{label}
</div>
)}
{ask.error !== undefined && ask.status !== 'running' && (
{ask.status === 'queued' && (
<div className="cr-qa-status" role="status">
<Hourglass size={12} strokeWidth={1.8} aria-hidden />
排队中,前面的写完就开始
</div>
)}
{ask.error !== undefined && !unfinished(ask) && (
<div className="cr-qa-error" role="alert">
<TriangleAlert size={13} strokeWidth={1.8} className="mt-[3px] shrink-0" aria-hidden />
<span>{ask.error}</span>
@@ -176,9 +182,10 @@ function CurrentItem({ ask, wording, onOpenImage }: { ask: Ask, wording: ThreadW
)
}
/** 底下的输入框:Enter 发送、Shift+Enter 换行、Esc 收起;正在答时发送换成停止。能贴图、拖图,贴的图在输入框上面一排,随下一个问题发 */
function AskInput({ running, placeholder, label, profiles, onSelectProfile, onAsk, onStop, onClose, closable, onOpenImage, autoFocus, focusToken }: {
running: Ask | undefined
/** 底下的输入框:Enter 发送、Shift+Enter 换行、Esc 收起;正在答时发送换成停止,排着队时换成取消排队。能贴图、拖图,贴的图在输入框上面一排,随下一个问题发 */
function AskInput({ active, placeholder, label, profiles, onSelectProfile, onAsk, onStop, onClose, closable, onOpenImage, autoFocus, focusToken }: {
/** 正在答或排着队的那一轮 */
active: Ask | undefined
/** 输入框里的提示(后面接着怎么发送、收起) */
placeholder: string
/** 输入框读屏用的名字 */
@@ -197,6 +204,7 @@ function AskInput({ running, placeholder, label, profiles, onSelectProfile, onAs
focusToken: number
}): ReactElement {
const [value, setValue] = useState('')
const stopLabel = active?.status === 'queued' ? '取消排队' : '停止'
const [images, setImages] = useState<PendingImage[]>([])
/** 贴的、拖的有没加上的:给人看的原因,下次贴图或发送时清掉 */
const [notice, setNotice] = useState<string>()
@@ -221,7 +229,7 @@ function AskInput({ running, placeholder, label, profiles, onSelectProfile, onAs
}
const send = (): void => {
const question = value.trim()
if (question === '' || running)
if (question === '' || active)
return
onAsk(question, images)
setValue('')
@@ -282,9 +290,9 @@ function AskInput({ running, placeholder, label, profiles, onSelectProfile, onAs
tooltip="用哪个 Claude Code 配置(profile)回答,下一个问题起用它"
className="cr-qa-profile"
/>
{running
{active
? (
<button type="button" className="cr-tool" data-tooltip="停止" aria-label="停止" onClick={() => onStop(running.askId)}>
<button type="button" className="cr-tool" data-tooltip={stopLabel} aria-label={stopLabel} onClick={() => onStop(active.askId)}>
<CircleStop size={14} strokeWidth={1.8} aria-hidden />
</button>
)
@@ -357,7 +365,7 @@ export function AskThread({ history, asks, open, profiles, hint = '这块代码'
{history.length > 0 && asks.length > 0 && <hr className="cr-qa-divider" />}
{asks.map(ask => <CurrentItem key={ask.askId} ask={ask} wording={wording} onOpenImage={setPreview} />)}
<AskInput
running={asks.find(ask => ask.status === 'running')}
active={asks.find(unfinished)}
placeholder={wording.placeholder(history.length === 0 && asks.length === 0, hint)}
label={wording.inputLabel}
profiles={profiles}