From 2ca32f319936e3b1678c7d05cf7a402727a5cea6 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Sat, 19 Sep 2026 00:17:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20Agent=20=E9=9D=A2=E6=9D=BF=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD=E6=B6=88=E6=81=AF=E6=8E=92=E9=98=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 忙时可继续输入并排队;立即打断当前任务、编辑或撤销; 结束后自动续发。补充 zh/en 文案与 docs/02。 --- docs/02-运行中消息排队.md | 30 +++++++ docs/README.md | 2 + src/components/ai/AiTerminalPanel.tsx | 117 +++++++++++++++++++++----- src/i18n/locales/en/ai.json | 9 +- src/i18n/locales/zh-CN/ai.json | 9 +- 5 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 docs/02-运行中消息排队.md diff --git a/docs/02-运行中消息排队.md b/docs/02-运行中消息排队.md new file mode 100644 index 0000000..a9dc48c --- /dev/null +++ b/docs/02-运行中消息排队.md @@ -0,0 +1,30 @@ +# 运行中消息排队(三产品统一) + +> // Logan.Li +> 更新:2026-09-19 +> 对标常见 Agent UX(Zcode / Cursor):当前任务仍在跑时,允许预写下一条。 + +## 语义 + +| 动作 | 含义 | +| ------------ | ---------------------------------------------------------------------------------------------------------------------- | +| **排队** | 运行中提交的新输入进入 `pending`(默认一条;再提交则覆盖或需先撤销) | +| **自动续发** | 当前任务 `completed` / `incomplete` / `failed` / `cancelled` 结束后,若仍有 pending,自动开始该条(continue/追问语义) | +| **立即** | 中断当前任务,立刻用 pending 内容开跑 | +| **编辑** | 把 pending 拉回输入框,清空排队槽 | +| **撤销** | 丢弃 pending,不发送 | + +与 [运行结果状态与可续跑契约]((见 opsxcli/aiops docs/13-运行结果状态与可续跑契约.md)) 的关系:排队是**同会话并发输入**;incomplete 续跑是**上一趟未完成再继续**。两者可叠加。 + +## 产品落点 + +| 产品 | UI | 中断手段 | +| ----------- | ------------------------------------- | ------------------------------------------ | +| opsxshell | Agent 面板排队条 | `stopAgent`(bump runId)后立刻 `runAgent` | +| aiops Web | AgentCLI 输入区排队条 | WS `abort` 后再 `continue`/`start` | +| opsxcli TUI | 忙时 Enter 入队;快捷键立即/编辑/撤销 | cancel 当前 RunStream ctx | + +## 不做 + +- 多条 FIFO 长队列(首版单槽) +- 跨会话排队 diff --git a/docs/README.md b/docs/README.md index f041b34..897f493 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,3 +5,5 @@ | [01-AI-Agent步数预算与incomplete](01-AI-Agent步数预算与incomplete.md) | MAX_AGENT_STEPS=12 与 incomplete/续跑目标 | 跨产品契约以 aiops/opsxcli 的 `13-运行结果状态与可续跑契约.md` 为准。 + +- [02-运行中消息排队](02-运行中消息排队.md) — 忙时排队 / 立即 / 编辑 / 撤销 diff --git a/src/components/ai/AiTerminalPanel.tsx b/src/components/ai/AiTerminalPanel.tsx index fd6cfb0..c96c09a 100644 --- a/src/components/ai/AiTerminalPanel.tsx +++ b/src/components/ai/AiTerminalPanel.tsx @@ -5,10 +5,12 @@ import { CheckCircle2, Clipboard, Loader2, + Pencil, Plus, Send, Sparkles, Square, + ArrowUp, TerminalSquare, TextSelect, X, @@ -182,6 +184,12 @@ export default function AiTerminalPanel({ const confirmAllCommands = useSettingsStore((state) => state.settings.agentConfirmAllCommands) const setSetting = useSettingsStore((state) => state.setSetting) const [prompt, setPrompt] = useState('') + const [queuedPrompt, setQueuedPrompt] = useState(null) + const queuedRef = useRef(null) + const setQueued = (v: string | null) => { + queuedRef.current = v + setQueuedPrompt(v) + } const [messages, setMessages] = useState([]) const [liveSteps, setLiveSteps] = useState([]) const [error, setError] = useState('') @@ -220,12 +228,13 @@ export default function AiTerminalPanel({ setLiveSteps([]) setError('') setPrompt('') + setQueued(null) setCopiedId('') sessionAllowRef.current.clear() initialSelectionRef.current = '' } - const runAgent = async () => { + const runAgent = async (taskOverride?: string) => { setError('') if (!provider || !model) { setError(t('error.noProvider')) @@ -235,7 +244,7 @@ export default function AiTerminalPanel({ setError(t('error.noApiKey', { provider: provider.name })) return } - const task = prompt.trim() + const task = (taskOverride ?? prompt).trim() if (!task) { setError(t('error.emptyTask')) return @@ -431,10 +440,54 @@ export default function AiTerminalPanel({ if (runIdRef.current === runId) { runConnectionIdRef.current = null setRunning(false) + // 自动续发排队消息(契约 14) // Logan.Li + const pending = queuedRef.current + if (pending && pending.trim()) { + const next = pending.trim() + setQueued(null) + queueMicrotask(() => { + void runAgent(next) + }) + } } } } + const enqueueOrSend = () => { + const task = prompt.trim() + if (!task) { + setError(t('error.emptyTask')) + return + } + if (running) { + setQueued(task) + setPrompt('') + setError('') + return + } + void runAgent(task) + } + + const sendQueuedNow = () => { + const task = queuedPrompt?.trim() + if (!task) return + setQueued(null) + stopAgent() + queueMicrotask(() => { + void runAgent(task) + }) + } + + const editQueued = () => { + if (!queuedPrompt) return + setPrompt(queuedPrompt) + setQueued(null) + } + + const cancelQueued = () => { + setQueued(null) + } + const copyMessage = async (id: string, content: string) => { await writeClipboardText(content) setCopiedId(id) @@ -642,32 +695,61 @@ export default function AiTerminalPanel({
+ {queuedPrompt ? ( +
+
{t('panel.queueHint')}
+
{queuedPrompt}
+
+ + + +
+
+ ) : null}