fix(h5-chat): queue main-session submits while agent is busy
Memind CI / Test, build, and release guards (push) Successful in 4m1s
Memind CI / Test, build, and release guards (push) Successful in 4m1s
Prevent silent drops when users send follow-up instructions during waiting or streaming by showing the message immediately, notifying them it is queued, and auto-flushing after the composer returns to idle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
<button type="button" className="danger-btn chat-input-action" onClick={() => void onStop()}>
|
||||
停止
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`send-btn chat-input-action${showHomeWelcome ? ' chat-input-action-home' : ''}`}
|
||||
disabled={!canSubmit || voiceDisabled || uploadingImage || uploadingFile}
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
{sendButtonLabel ?? '发送'}
|
||||
</button>
|
||||
)}
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={`send-btn chat-input-action${showHomeWelcome ? ' chat-input-action-home' : ''}`}
|
||||
disabled={!canSubmit || inputBlocked || uploadingImage || uploadingFile}
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
{sendButtonLabel ?? '发送'}
|
||||
</button>
|
||||
</div>
|
||||
{compact && onClose && (
|
||||
<button type="button" className="space-chat-panel-dismiss" onClick={onClose}>
|
||||
|
||||
+173
-81
@@ -58,6 +58,9 @@ import {
|
||||
buildAutoChatSkillPrefix,
|
||||
} from '../../chat-skills.mjs';
|
||||
import {
|
||||
buildQueuedChatSubmitNotice,
|
||||
canFlushQueuedChatSubmit,
|
||||
isChatSubmitBusy,
|
||||
reconcileSessionEventRequestContext,
|
||||
resolvePostAgentRunChatState,
|
||||
shouldIgnoreZeroActivityFinish,
|
||||
@@ -89,6 +92,24 @@ import {
|
||||
touchSession,
|
||||
} from '../utils/sessions';
|
||||
|
||||
type ChatSubmitOptions = {
|
||||
mindspaceContext?: MindSpaceChatContext;
|
||||
messageId?: string;
|
||||
forceDeepReasoning?: boolean;
|
||||
pgRequired?: boolean;
|
||||
imageGenerationMode?: ImageGenerationMode;
|
||||
selectedChatSkill?: string;
|
||||
fileAttachments?: ChatFileAttachment[];
|
||||
goalRunId?: string;
|
||||
};
|
||||
|
||||
type PendingChatSubmitEntry = {
|
||||
userMessage: Message;
|
||||
options?: ChatSubmitOptions;
|
||||
normalizedImageUrls: string[];
|
||||
normalizedFileAttachments: ChatFileAttachment[];
|
||||
};
|
||||
|
||||
const INSUFFICIENT_BALANCE_NOTICE = '余额不足,请充值后继续使用';
|
||||
const REPLY_RECOVERY_SYNC_DELAYS_MS = [1500, 5000, 12000];
|
||||
const FINISH_SYNC_RETRY_DELAYS_MS = [500, 1500, 3000];
|
||||
@@ -400,11 +421,18 @@ export function useTKMindChat(
|
||||
const onUserUpdateRef = useRef(onUserUpdate);
|
||||
const chatImageCategoryIdRef = useRef<string | null>(null);
|
||||
const chatFileCategoryIdRef = useRef<string | null>(null);
|
||||
const pendingSubmitQueueRef = useRef<PendingChatSubmitEntry[]>([]);
|
||||
const flushingPendingSubmitRef = useRef(false);
|
||||
const pendingToolRef = useRef<ToolConfirmation | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
chatStateRef.current = chatState;
|
||||
}, [chatState]);
|
||||
|
||||
useEffect(() => {
|
||||
pendingToolRef.current = pendingTool;
|
||||
}, [pendingTool]);
|
||||
|
||||
const clearActiveRequestMissingTimer = useCallback(() => {
|
||||
if (!activeRequestMissingTimerRef.current) return;
|
||||
window.clearTimeout(activeRequestMissingTimerRef.current);
|
||||
@@ -1175,6 +1203,8 @@ export function useTKMindChat(
|
||||
unsubscribeRef.current = null;
|
||||
subscribedSessionIdRef.current = null;
|
||||
clearActiveRequestMissingTimer();
|
||||
pendingSubmitQueueRef.current = [];
|
||||
flushingPendingSubmitRef.current = false;
|
||||
setError(null);
|
||||
setPendingTool(null);
|
||||
agentRunPendingRef.current = false;
|
||||
@@ -1478,84 +1508,23 @@ export function useTKMindChat(
|
||||
[session, chatState, connectSession, resetSessionView, sessions],
|
||||
);
|
||||
|
||||
const submit = useCallback(
|
||||
const executeAgentSubmit = useCallback(
|
||||
async (
|
||||
text: string,
|
||||
options?: {
|
||||
mindspaceContext?: MindSpaceChatContext;
|
||||
messageId?: string;
|
||||
forceDeepReasoning?: boolean;
|
||||
pgRequired?: boolean;
|
||||
imageGenerationMode?: ImageGenerationMode;
|
||||
selectedChatSkill?: string;
|
||||
fileAttachments?: ChatFileAttachment[];
|
||||
goalRunId?: string;
|
||||
},
|
||||
imageUrls?: string[],
|
||||
previewImageUrls?: string[],
|
||||
userMessage: Message,
|
||||
options: ChatSubmitOptions | undefined,
|
||||
normalizedImageUrls: string[],
|
||||
normalizedFileAttachments: ChatFileAttachment[],
|
||||
) => {
|
||||
const normalizedImageUrls = (imageUrls ?? []).filter((value) => typeof value === 'string' && value.trim());
|
||||
const normalizedPreviewImageUrls = (previewImageUrls ?? []).filter(
|
||||
(value) => typeof value === 'string' && value.trim(),
|
||||
);
|
||||
const normalizedFileAttachments = (options?.fileAttachments ?? []).filter(
|
||||
(item) => item?.downloadUrl?.trim() && item?.filename?.trim(),
|
||||
);
|
||||
if (!text.trim() && normalizedImageUrls.length === 0 && normalizedFileAttachments.length === 0) return;
|
||||
// Use the ref here (not the React state) so that rapid back-to-back calls in the
|
||||
// same render cycle are blocked even before the state update has been re-rendered.
|
||||
if (
|
||||
chatStateRef.current === 'streaming' ||
|
||||
chatStateRef.current === 'loading' ||
|
||||
chatStateRef.current === 'connecting' ||
|
||||
chatStateRef.current === 'waiting'
|
||||
) return;
|
||||
|
||||
const trimmed = text.trim();
|
||||
const mindspacePrefix = options?.mindspaceContext
|
||||
? buildContextPrefix(options.mindspaceContext)
|
||||
: '';
|
||||
const userPrefix = buildUserAddressPrefix(userRef.current);
|
||||
const skillPrefix = buildAutoChatSkillPrefix(trimmed, grantedSkills ?? []);
|
||||
const pgContractPrefix = options?.pgRequired
|
||||
? '[交付约束:用户已明确要求使用专属 PostgreSQL 数据空间。若生成页面,必须按 page-data-collect 完成建表、dataset、policy 和 workspace page 绑定;禁止 localStorage、SQLite、静态 JSON 或内存持久化。所有验证通过前不得回复已发布或给出页面链接。]\n'
|
||||
: '';
|
||||
const agentPrefix = `${userPrefix}${mindspacePrefix}${skillPrefix}${pgContractPrefix}`;
|
||||
const priorMessageCount = messagesRef.current.length;
|
||||
const userMessage = buildUserMessage(trimmed, {
|
||||
id: options?.messageId,
|
||||
agentText: `${agentPrefix}${trimmed}`,
|
||||
displayText: trimmed,
|
||||
imageUrls: normalizedImageUrls,
|
||||
previewImageUrls: normalizedPreviewImageUrls,
|
||||
fileAttachments: normalizedFileAttachments,
|
||||
});
|
||||
userMessage.metadata = {
|
||||
...userMessage.metadata,
|
||||
memindRun: {
|
||||
...(userMessage.metadata?.memindRun && typeof userMessage.metadata.memindRun === 'object'
|
||||
? userMessage.metadata.memindRun
|
||||
: {}),
|
||||
sessionMessageCount: priorMessageCount,
|
||||
...(options?.pgRequired ? { pgRequired: true } : {}),
|
||||
imageGenerationMode: options?.imageGenerationMode ?? 'auto',
|
||||
...(options?.selectedChatSkill ? { selectedChatSkill: options.selectedChatSkill } : {}),
|
||||
},
|
||||
};
|
||||
const trimmed = getDisplayText(userMessage).trim();
|
||||
const requestId = crypto.randomUUID();
|
||||
const submitToken = connectTokenRef.current;
|
||||
activeRequestId.current = requestId;
|
||||
messagesRef.current = [...messagesRef.current, userMessage];
|
||||
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||
setMessages(messagesRef.current);
|
||||
setChatState('waiting');
|
||||
// Immediately reflect in the ref so any synchronous re-entry is blocked before
|
||||
// the next React render cycle runs the useEffect that normally syncs this ref.
|
||||
chatStateRef.current = 'waiting';
|
||||
setError(null);
|
||||
setPendingTool(null);
|
||||
|
||||
let activeSessionId = session?.id ?? null;
|
||||
let activeSessionId = sessionRef.current?.id ?? null;
|
||||
|
||||
if (activeSessionId) {
|
||||
setSessions((prev) => touchSession(prev, activeSessionId!, 1));
|
||||
@@ -1634,14 +1603,15 @@ export function useTKMindChat(
|
||||
});
|
||||
if (submitToken !== connectTokenRef.current) return;
|
||||
agentRunPendingRef.current = false;
|
||||
activeSessionId = finishedRun.sessionId;
|
||||
activeSessionId = finishedRun.sessionId ?? activeSessionId;
|
||||
if (!activeSessionId) {
|
||||
throw new Error('后台任务已提交,但未返回会话');
|
||||
}
|
||||
if (normalizedImageUrls.length > 0 || normalizedFileAttachments.length > 0) {
|
||||
void claimMindSpaceConversationUploads(activeSessionId, userMessage.id).catch(() => {});
|
||||
}
|
||||
if (!session?.id || session.id !== activeSessionId) {
|
||||
const currentSessionId = sessionRef.current?.id ?? null;
|
||||
if (!currentSessionId || currentSessionId !== activeSessionId) {
|
||||
const nextSession: Session = {
|
||||
id: activeSessionId,
|
||||
name: 'New Chat',
|
||||
@@ -1700,10 +1670,6 @@ export function useTKMindChat(
|
||||
const nextChatState = resolvePostAgentRunChatState({
|
||||
chatState: chatStateRef.current,
|
||||
finishedViaPortalDirectChat,
|
||||
// The agent-run result is authoritative even when the immediate
|
||||
// session snapshot has not yet carried portal-direct metadata.
|
||||
// Without this, a completed Page Data task can re-enter streaming
|
||||
// and leave the Stop button attached to no active request.
|
||||
agentRunSucceeded: finishedRun.status === 'succeeded',
|
||||
});
|
||||
if (nextChatState === 'idle') {
|
||||
@@ -1735,9 +1701,6 @@ export function useTKMindChat(
|
||||
errorCode(err),
|
||||
)
|
||||
) {
|
||||
// Goose may report its session concurrency guard as a failed run
|
||||
// message instead of an HTTP 409. Reattach to the session stream
|
||||
// and reconcile the snapshot; do not strand the composer in error.
|
||||
subscribeToSession(activeSessionId);
|
||||
setChatState('streaming');
|
||||
setError(null);
|
||||
@@ -1745,7 +1708,9 @@ export function useTKMindChat(
|
||||
return;
|
||||
}
|
||||
agentRunPendingRef.current = false;
|
||||
if (session && activeSessionId) setSessions((prev) => touchSession(prev, activeSessionId!, -1));
|
||||
if (sessionRef.current && activeSessionId) {
|
||||
setSessions((prev) => touchSession(prev, activeSessionId!, -1));
|
||||
}
|
||||
if (err instanceof ApiError && err.status === 402) {
|
||||
notifyInsufficientBalance();
|
||||
} else {
|
||||
@@ -1758,18 +1723,143 @@ export function useTKMindChat(
|
||||
},
|
||||
[
|
||||
notifyInsufficientBalance,
|
||||
session,
|
||||
grantedSkills,
|
||||
clearActiveRequestMissingTimer,
|
||||
subscribeToSession,
|
||||
scheduleReplyRecoverySync,
|
||||
ensureProvider,
|
||||
loadProjectMemory,
|
||||
refreshSessions,
|
||||
syncSessionMessages,
|
||||
],
|
||||
);
|
||||
|
||||
const flushPendingSubmitQueue = useCallback(async () => {
|
||||
if (flushingPendingSubmitRef.current) return;
|
||||
if (
|
||||
!canFlushQueuedChatSubmit({
|
||||
chatState: chatStateRef.current,
|
||||
agentRunPending: agentRunPendingRef.current,
|
||||
pendingTool: Boolean(pendingToolRef.current),
|
||||
queueLength: pendingSubmitQueueRef.current.length,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = pendingSubmitQueueRef.current.shift();
|
||||
if (!next) return;
|
||||
|
||||
flushingPendingSubmitRef.current = true;
|
||||
try {
|
||||
await executeAgentSubmit(
|
||||
next.userMessage,
|
||||
next.options,
|
||||
next.normalizedImageUrls,
|
||||
next.normalizedFileAttachments,
|
||||
);
|
||||
} finally {
|
||||
flushingPendingSubmitRef.current = false;
|
||||
if (
|
||||
canFlushQueuedChatSubmit({
|
||||
chatState: chatStateRef.current,
|
||||
agentRunPending: agentRunPendingRef.current,
|
||||
pendingTool: Boolean(pendingToolRef.current),
|
||||
queueLength: pendingSubmitQueueRef.current.length,
|
||||
})
|
||||
) {
|
||||
void flushPendingSubmitQueue();
|
||||
}
|
||||
}
|
||||
}, [executeAgentSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!canFlushQueuedChatSubmit({
|
||||
chatState,
|
||||
agentRunPending: agentRunPendingRef.current,
|
||||
pendingTool: Boolean(pendingTool),
|
||||
queueLength: pendingSubmitQueueRef.current.length,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void flushPendingSubmitQueue();
|
||||
}, [chatState, pendingTool, flushPendingSubmitQueue]);
|
||||
|
||||
const submit = useCallback(
|
||||
async (
|
||||
text: string,
|
||||
options?: ChatSubmitOptions,
|
||||
imageUrls?: string[],
|
||||
previewImageUrls?: string[],
|
||||
) => {
|
||||
const normalizedImageUrls = (imageUrls ?? []).filter((value) => typeof value === 'string' && value.trim());
|
||||
const normalizedPreviewImageUrls = (previewImageUrls ?? []).filter(
|
||||
(value) => typeof value === 'string' && value.trim(),
|
||||
);
|
||||
const normalizedFileAttachments = (options?.fileAttachments ?? []).filter(
|
||||
(item) => item?.downloadUrl?.trim() && item?.filename?.trim(),
|
||||
);
|
||||
if (!text.trim() && normalizedImageUrls.length === 0 && normalizedFileAttachments.length === 0) return;
|
||||
|
||||
const trimmed = text.trim();
|
||||
const mindspacePrefix = options?.mindspaceContext
|
||||
? buildContextPrefix(options.mindspaceContext)
|
||||
: '';
|
||||
const userPrefix = buildUserAddressPrefix(userRef.current);
|
||||
const skillPrefix = buildAutoChatSkillPrefix(trimmed, grantedSkills ?? []);
|
||||
const pgContractPrefix = options?.pgRequired
|
||||
? '[交付约束:用户已明确要求使用专属 PostgreSQL 数据空间。若生成页面,必须按 page-data-collect 完成建表、dataset、policy 和 workspace page 绑定;禁止 localStorage、SQLite、静态 JSON 或内存持久化。所有验证通过前不得回复已发布或给出页面链接。]\n'
|
||||
: '';
|
||||
const agentPrefix = `${userPrefix}${mindspacePrefix}${skillPrefix}${pgContractPrefix}`;
|
||||
const priorMessageCount = messagesRef.current.length;
|
||||
const userMessage = buildUserMessage(trimmed, {
|
||||
id: options?.messageId,
|
||||
agentText: `${agentPrefix}${trimmed}`,
|
||||
displayText: trimmed,
|
||||
imageUrls: normalizedImageUrls,
|
||||
previewImageUrls: normalizedPreviewImageUrls,
|
||||
fileAttachments: normalizedFileAttachments,
|
||||
});
|
||||
userMessage.metadata = {
|
||||
...userMessage.metadata,
|
||||
memindRun: {
|
||||
...(userMessage.metadata?.memindRun && typeof userMessage.metadata.memindRun === 'object'
|
||||
? userMessage.metadata.memindRun
|
||||
: {}),
|
||||
sessionMessageCount: priorMessageCount,
|
||||
...(options?.pgRequired ? { pgRequired: true } : {}),
|
||||
imageGenerationMode: options?.imageGenerationMode ?? 'auto',
|
||||
...(options?.selectedChatSkill ? { selectedChatSkill: options.selectedChatSkill } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
if (isChatSubmitBusy(chatStateRef.current)) {
|
||||
pendingSubmitQueueRef.current.push({
|
||||
userMessage,
|
||||
options,
|
||||
normalizedImageUrls,
|
||||
normalizedFileAttachments,
|
||||
});
|
||||
messagesRef.current = [...messagesRef.current, userMessage];
|
||||
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||
setMessages(messagesRef.current);
|
||||
setNotice(buildQueuedChatSubmitNotice(pendingSubmitQueueRef.current.length));
|
||||
return;
|
||||
}
|
||||
|
||||
messagesRef.current = [...messagesRef.current, userMessage];
|
||||
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||
setMessages(messagesRef.current);
|
||||
await executeAgentSubmit(
|
||||
userMessage,
|
||||
options,
|
||||
normalizedImageUrls,
|
||||
normalizedFileAttachments,
|
||||
);
|
||||
},
|
||||
[executeAgentSubmit, grantedSkills],
|
||||
);
|
||||
|
||||
const stop = useCallback(async () => {
|
||||
if (!session || !activeRequestId.current) return;
|
||||
try {
|
||||
@@ -1894,6 +1984,8 @@ export function useTKMindChat(
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
agentRunPendingRef.current = false;
|
||||
activeRequestId.current = null;
|
||||
pendingSubmitQueueRef.current = [];
|
||||
flushingPendingSubmitRef.current = false;
|
||||
messagesRef.current = [];
|
||||
messageHistoryLoadedCountRef.current = 0;
|
||||
messageHistoryTotalRef.current = 0;
|
||||
|
||||
Reference in New Issue
Block a user