diff --git a/chat-agent-run-gate.mjs b/chat-agent-run-gate.mjs index 7e3705a..db33a32 100644 --- a/chat-agent-run-gate.mjs +++ b/chat-agent-run-gate.mjs @@ -72,6 +72,49 @@ export function shouldScheduleMissingActiveRequestGrace({ return allowMissingGrace && !agentRunPending; } +export const QUEUED_CHAT_SUBMIT_NOTICE = + '已收到你的消息,将在当前任务完成后自动继续执行。'; + +/** + * @param {string | undefined | null} chatState + * @returns {boolean} + */ +export function isChatSubmitBusy(chatState) { + return ( + chatState === 'streaming' || + chatState === 'loading' || + chatState === 'connecting' || + chatState === 'waiting' + ); +} + +/** + * @param {number | undefined | null} queueLength + * @returns {string} + */ +export function buildQueuedChatSubmitNotice(queueLength = 1) { + const length = Math.max(1, Number(queueLength) || 1); + if (length <= 1) return QUEUED_CHAT_SUBMIT_NOTICE; + return `已收到你的消息,当前还有 ${length} 条待执行,将在任务完成后按顺序继续。`; +} + +/** + * @param {{ chatState?: string; agentRunPending?: boolean; pendingTool?: boolean; queueLength?: number }} input + * @returns {boolean} + */ +export function canFlushQueuedChatSubmit({ + chatState = 'idle', + agentRunPending = false, + pendingTool = false, + queueLength = 0, +} = {}) { + if (!queueLength || queueLength <= 0) return false; + if (isChatSubmitBusy(chatState)) return false; + if (agentRunPending) return false; + if (pendingTool) return false; + return true; +} + /** * Only transport uncertainty may continue a run after submit fails. A * deterministic gateway error already has a terminal outcome and must return diff --git a/chat-agent-run-gate.test.mjs b/chat-agent-run-gate.test.mjs index df43ee5..1fe0b78 100644 --- a/chat-agent-run-gate.test.mjs +++ b/chat-agent-run-gate.test.mjs @@ -1,6 +1,9 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + buildQueuedChatSubmitNotice, + canFlushQueuedChatSubmit, + isChatSubmitBusy, reconcileSessionEventRequestContext, resolvePostAgentRunChatState, shouldIgnoreZeroActivityFinish, @@ -145,6 +148,52 @@ test('missing ActiveRequests cannot unlock while the Portal agent-run is pending ); }); +test('isChatSubmitBusy covers active composer states', () => { + assert.equal(isChatSubmitBusy('idle'), false); + assert.equal(isChatSubmitBusy('error'), false); + assert.equal(isChatSubmitBusy('waiting'), true); + assert.equal(isChatSubmitBusy('streaming'), true); + assert.equal(isChatSubmitBusy('connecting'), true); +}); + +test('buildQueuedChatSubmitNotice reflects queue depth', () => { + assert.match(buildQueuedChatSubmitNotice(1), /当前任务完成后/); + assert.match(buildQueuedChatSubmitNotice(2), /2 条待执行/); +}); + +test('canFlushQueuedChatSubmit waits for idle composer without pending tool or run', () => { + assert.equal( + canFlushQueuedChatSubmit({ + chatState: 'idle', + queueLength: 1, + }), + true, + ); + assert.equal( + canFlushQueuedChatSubmit({ + chatState: 'waiting', + queueLength: 1, + }), + false, + ); + assert.equal( + canFlushQueuedChatSubmit({ + chatState: 'idle', + agentRunPending: true, + queueLength: 1, + }), + false, + ); + assert.equal( + canFlushQueuedChatSubmit({ + chatState: 'idle', + pendingTool: true, + queueLength: 1, + }), + false, + ); +}); + test('reconcileSessionEventRequestContext adopts Goose request id while agent-run gate waits', () => { assert.deepEqual( reconcileSessionEventRequestContext({ diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 6ce0a81..ed7903a 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -492,6 +492,7 @@ export function ChatPanel({ chatState === 'connecting' || chatState === 'waiting'; const offlineBlocked = !online; + const inputBlocked = !!pendingTool || chatState === 'error' || offlineBlocked; const publishSkillName = user?.publishSkillName ?? 'static-page-publish'; const hasPublishSkill = grantedSkills?.includes(publishSkillName) ?? false; const hasPageDataCollectSkill = grantedSkills?.includes('page-data-collect') ?? false; @@ -525,9 +526,9 @@ export function ChatPanel({ ? '上传中…' : chatState === 'connecting' ? '连接中…' - : chatState === 'waiting' - ? '提交中…' - : null; + : busy && canSubmit + ? '排队发送' + : null; const applyTemplatePrefill = useCallback((prompt: string, skillId: string, label: string) => { pendingSkillRef.current = skillId; @@ -577,7 +578,7 @@ export function ChatPanel({ setVoiceNotice('已识别,可编辑后发送'); }; - const voiceDisabled = busy || !!pendingTool || chatState === 'error' || offlineBlocked; + const voiceDisabled = inputBlocked || uploadingImage || uploadingFile; const canSubmit = input.trim() || pendingImages.length > 0 || pendingFiles.length > 0; const uploadDisabled = !canUpload || busy || uploadingImage || uploadingFile || offlineBlocked || !!pendingTool; @@ -719,7 +720,7 @@ export function ChatPanel({ skillIdOverride ?? pendingSkillRef.current ?? templateSelection?.skillId ?? undefined; pendingSkillRef.current = null; setActiveTemplatePrefill(null); - if ((!trimmed && pendingImages.length === 0 && pendingFiles.length === 0) || voiceDisabled) return; + if ((!trimmed && pendingImages.length === 0 && pendingFiles.length === 0) || inputBlocked) return; if (pendingImages.length > 0 && !onUploadImage) { setImageError('当前会话暂不支持图片发送'); return; @@ -1462,16 +1463,15 @@ export function ChatPanel({ - ) : ( - - )} + ) : null} + {compact && onClose && (