feat: add message queue for sequential run processing (#501)
Allow sending multiple messages while a run is active. Messages are queued on the server and processed sequentially after each run completes. Each completed assistant message triggers speech playback independently, and the UI shows queue status with a badge indicator. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ export interface Message {
|
||||
// 2) 流式:由 reasoning.delta / thinking.delta / reasoning.available 事件累加
|
||||
// 不含 <think> 包裹标签;内容自身可以为多段纯文本。
|
||||
reasoning?: string
|
||||
queued?: boolean
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
@@ -312,6 +313,10 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const streamStates = ref<Map<string, { abort: () => void }>>(new Map())
|
||||
/** sessionId → server-reported isWorking status */
|
||||
const serverWorking = ref<Set<string>>(new Set())
|
||||
/** sessionId → queued message count */
|
||||
const queueLengths = ref<Map<string, number>>(new Map())
|
||||
/** sessionId → queued user messages not yet visible in the transcript */
|
||||
const queuedUserMessages = ref<Map<string, Message[]>>(new Map())
|
||||
|
||||
// 自动播放语音开关
|
||||
const autoPlaySpeechEnabled = ref(false)
|
||||
@@ -448,6 +453,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
} else {
|
||||
serverWorking.value.delete(sessionId)
|
||||
}
|
||||
if (data.queueLength && data.queueLength > 0) {
|
||||
queueLengths.value.set(sessionId, data.queueLength)
|
||||
} else {
|
||||
queueLengths.value.delete(sessionId)
|
||||
}
|
||||
if ((data as any).isAborting) {
|
||||
setAbortState({ aborting: true, synced: null })
|
||||
} else if (!data.isWorking) {
|
||||
@@ -568,6 +578,41 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function enqueueUserMessage(sessionId: string, message: Message) {
|
||||
const queue = queuedUserMessages.value.get(sessionId) || []
|
||||
queue.push({ ...message, queued: true })
|
||||
queuedUserMessages.value.set(sessionId, queue)
|
||||
}
|
||||
|
||||
function removeQueuedMessage(sessionId: string, messageId: string) {
|
||||
const queue = queuedUserMessages.value.get(sessionId)
|
||||
if (!queue?.length) return
|
||||
const next = queue.filter(message => message.id !== messageId)
|
||||
if (next.length > 0) {
|
||||
queuedUserMessages.value.set(sessionId, next)
|
||||
} else {
|
||||
queuedUserMessages.value.delete(sessionId)
|
||||
}
|
||||
queueLengths.value.set(sessionId, next.length)
|
||||
getChatRunSocket()?.emit('cancel_queued_run', {
|
||||
session_id: sessionId,
|
||||
queue_id: messageId,
|
||||
})
|
||||
}
|
||||
|
||||
function showNextQueuedUserMessage(sessionId: string) {
|
||||
const queue = queuedUserMessages.value.get(sessionId)
|
||||
if (!queue?.length) return
|
||||
const next = queue.shift()!
|
||||
if (queue.length > 0) {
|
||||
queuedUserMessages.value.set(sessionId, queue)
|
||||
} else {
|
||||
queuedUserMessages.value.delete(sessionId)
|
||||
}
|
||||
addMessage(sessionId, { ...next, queued: false })
|
||||
updateSessionTitle(sessionId)
|
||||
}
|
||||
|
||||
function updateSessionTitle(sessionId: string) {
|
||||
const target = sessions.value.find(s => s.id === sessionId)
|
||||
if (!target) return
|
||||
@@ -596,7 +641,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
async function sendMessage(content: string, attachments?: Attachment[]) {
|
||||
if ((!content.trim() && !(attachments && attachments.length > 0)) || isStreaming.value) return
|
||||
if ((!content.trim() && !(attachments && attachments.length > 0))) return
|
||||
|
||||
primeCompletionBellIfEnabled()
|
||||
|
||||
@@ -607,6 +652,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
// Capture session ID at send time — all callbacks use this, not activeSessionId
|
||||
const sid = activeSessionId.value!
|
||||
const shouldQueue = isSessionLive(sid)
|
||||
|
||||
const userMsg: Message = {
|
||||
id: uid(),
|
||||
@@ -614,12 +660,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
content: content.trim(),
|
||||
timestamp: Date.now(),
|
||||
attachments: attachments && attachments.length > 0 ? attachments : undefined,
|
||||
queued: shouldQueue,
|
||||
}
|
||||
|
||||
addMessage(sid, userMsg)
|
||||
|
||||
|
||||
updateSessionTitle(sid)
|
||||
if (!shouldQueue) {
|
||||
addMessage(sid, userMsg)
|
||||
updateSessionTitle(sid)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -635,13 +682,20 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const base = `/api/hermes/download?path=${encodeURIComponent(f.path)}&name=${encodeURIComponent(f.name)}`
|
||||
return [f.name, token ? `${base}&token=${encodeURIComponent(token)}` : base]
|
||||
}))
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastUser = msgs.findLast(m => m.id === userMsg.id)
|
||||
if (lastUser?.attachments) {
|
||||
lastUser.attachments = lastUser.attachments.map(a => {
|
||||
if (shouldQueue && userMsg.attachments) {
|
||||
userMsg.attachments = userMsg.attachments.map(a => {
|
||||
const dl = urlMap.get(a.name)
|
||||
return dl ? { ...a, url: dl } : a
|
||||
})
|
||||
} else {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastUser = msgs.findLast(m => m.id === userMsg.id)
|
||||
if (lastUser?.attachments) {
|
||||
lastUser.attachments = lastUser.attachments.map(a => {
|
||||
const dl = urlMap.get(a.name)
|
||||
return dl ? { ...a, url: dl } : a
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Build content blocks with uploaded file paths
|
||||
@@ -657,6 +711,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
input,
|
||||
session_id: sid,
|
||||
model: sessionModel || undefined,
|
||||
queue_id: userMsg.id,
|
||||
}
|
||||
|
||||
if (shouldQueue) {
|
||||
enqueueUserMessage(sid, userMsg)
|
||||
}
|
||||
|
||||
// Helper to clean up this session's stream state
|
||||
@@ -665,15 +724,29 @@ export const useChatStore = defineStore('chat', () => {
|
||||
serverWorking.value.delete(sid)
|
||||
}
|
||||
|
||||
// Per-run flags used to detect silently-swallowed errors at run.completed.
|
||||
// Per-active-run flags used to detect silently-swallowed errors at run.completed.
|
||||
// hermes-agent occasionally emits run.completed with empty output and no
|
||||
// usage when the agent layer caught an upstream error (e.g. invalid API
|
||||
// key). We need to distinguish: (a) run with assistant text produced,
|
||||
// (b) run with only tool activity, (c) run with truly nothing visible.
|
||||
// Reset per send() call — closures captured by Socket.IO callbacks are scoped
|
||||
// to this run, so there is no cross-run contamination.
|
||||
// Reset on every run.started because one handler may span multiple queued runs.
|
||||
let runProducedAssistantText = false
|
||||
let runHadToolActivity = false
|
||||
let activeAssistantMessageId: string | null = null
|
||||
|
||||
const startNextQueuedUser = () => {
|
||||
showNextQueuedUserMessage(sid)
|
||||
}
|
||||
|
||||
const closeStreamingAssistant = () => {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
msgs.forEach(m => {
|
||||
if (m.role === 'assistant' && m.isStreaming) {
|
||||
updateMessage(sid, m.id, { isStreaming: false })
|
||||
}
|
||||
})
|
||||
activeAssistantMessageId = null
|
||||
}
|
||||
|
||||
// Send run via Socket.IO and listen to streamed events — all closures capture `sid`
|
||||
const ctrl = startRunViaSocket(
|
||||
@@ -682,8 +755,23 @@ export const useChatStore = defineStore('chat', () => {
|
||||
(evt: RunEvent) => {
|
||||
switch (evt.event) {
|
||||
case 'run.started':
|
||||
setAbortState(null)
|
||||
runProducedAssistantText = false
|
||||
runHadToolActivity = false
|
||||
closeStreamingAssistant()
|
||||
startNextQueuedUser()
|
||||
if ((evt as any).queue_length > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length)
|
||||
} else {
|
||||
queueLengths.value.delete(sid)
|
||||
}
|
||||
break
|
||||
|
||||
case 'run.queued': {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length || 0)
|
||||
break
|
||||
}
|
||||
|
||||
case 'compression.started': {
|
||||
setCompressionState({
|
||||
compressing: true,
|
||||
@@ -720,6 +808,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
case 'abort.completed': {
|
||||
setAbortState({ aborting: false, synced: (evt as any).synced ?? false })
|
||||
if ((evt as any).queue_length > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length)
|
||||
setAbortState(null)
|
||||
break
|
||||
}
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastMsg = msgs[msgs.length - 1]
|
||||
if (lastMsg?.isStreaming) {
|
||||
@@ -744,7 +837,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!text) break
|
||||
runProducedAssistantText = true
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const last = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: null
|
||||
if (last?.role === 'assistant' && last.isStreaming) {
|
||||
last.reasoning = (last.reasoning || '') + text
|
||||
noteReasoningStart(last.id)
|
||||
@@ -758,6 +853,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
isStreaming: true,
|
||||
reasoning: text,
|
||||
})
|
||||
activeAssistantMessageId = newId
|
||||
noteReasoningStart(newId)
|
||||
}
|
||||
|
||||
@@ -784,7 +880,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'message.delta': {
|
||||
if (evt.delta) runProducedAssistantText = true
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const last = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: null
|
||||
if (last?.role === 'assistant' && last.isStreaming) {
|
||||
const prev = last.content
|
||||
const next = prev + (evt.delta || '')
|
||||
@@ -803,6 +901,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
timestamp: Date.now(),
|
||||
isStreaming: true,
|
||||
})
|
||||
activeAssistantMessageId = newId
|
||||
}
|
||||
|
||||
break
|
||||
@@ -811,10 +910,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'tool.started': {
|
||||
runHadToolActivity = true
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const last = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: msgs[msgs.length - 1]
|
||||
if (last?.isStreaming) {
|
||||
updateMessage(sid, last.id, { isStreaming: false })
|
||||
}
|
||||
activeAssistantMessageId = null
|
||||
addMessage(sid, {
|
||||
id: uid(),
|
||||
role: 'tool',
|
||||
@@ -850,7 +952,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
case 'run.completed': {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastMsg = msgs[msgs.length - 1]
|
||||
const lastMsg = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: msgs[msgs.length - 1]
|
||||
if (lastMsg?.isStreaming) {
|
||||
updateMessage(sid, lastMsg.id, { isStreaming: false })
|
||||
}
|
||||
@@ -873,7 +977,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if ((evt as any).parsed_content !== undefined) {
|
||||
// Backend has parsed stringified array format, update last assistant message
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')
|
||||
const lastAssistant = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: [...msgs].reverse().find(m => m.role === 'assistant')
|
||||
if (lastAssistant) {
|
||||
updateMessage(sid, lastAssistant.id, {
|
||||
content: (evt as any).parsed_content || '',
|
||||
@@ -936,7 +1042,12 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
cleanup()
|
||||
if ((evt as any).queue_remaining > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_remaining)
|
||||
} else {
|
||||
cleanup()
|
||||
}
|
||||
activeAssistantMessageId = null
|
||||
updateSessionTitle(sid)
|
||||
break
|
||||
}
|
||||
@@ -963,7 +1074,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
msgs[i] = { ...m, toolStatus: 'error' }
|
||||
}
|
||||
})
|
||||
cleanup()
|
||||
if ((evt as any).queue_remaining > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_remaining)
|
||||
} else {
|
||||
cleanup()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1033,6 +1148,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
let closed = false
|
||||
let runProducedAssistantText = false
|
||||
let runHadToolActivity = false
|
||||
let activeAssistantMessageId: string | null = null
|
||||
|
||||
const cleanup = () => {
|
||||
if (closed) return
|
||||
@@ -1043,13 +1159,42 @@ export const useChatStore = defineStore('chat', () => {
|
||||
unregisterSessionHandlers(sid)
|
||||
}
|
||||
|
||||
const startNextQueuedUser = () => {
|
||||
showNextQueuedUserMessage(sid)
|
||||
}
|
||||
|
||||
const closeStreamingAssistant = () => {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
msgs.forEach(m => {
|
||||
if (m.role === 'assistant' && m.isStreaming) {
|
||||
updateMessage(sid, m.id, { isStreaming: false })
|
||||
}
|
||||
})
|
||||
activeAssistantMessageId = null
|
||||
}
|
||||
|
||||
// Shared event handler — filters by session_id tag
|
||||
function handleEvent(evt: RunEvent) {
|
||||
if (closed) return
|
||||
// Filter events for this session (server tags all events with session_id)
|
||||
if (evt.session_id && evt.session_id !== sid) return
|
||||
switch (evt.event) {
|
||||
case 'run.queued': {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length || 0)
|
||||
break
|
||||
}
|
||||
|
||||
case 'run.started':
|
||||
setAbortState(null)
|
||||
runProducedAssistantText = false
|
||||
runHadToolActivity = false
|
||||
closeStreamingAssistant()
|
||||
startNextQueuedUser()
|
||||
if ((evt as any).queue_length > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length)
|
||||
} else {
|
||||
queueLengths.value.delete(sid)
|
||||
}
|
||||
break
|
||||
|
||||
case 'compression.started': {
|
||||
@@ -1087,6 +1232,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
case 'abort.completed': {
|
||||
setAbortState({ aborting: false, synced: (evt as any).synced ?? false })
|
||||
if ((evt as any).queue_length > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length)
|
||||
setAbortState(null)
|
||||
break
|
||||
}
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastMsg = msgs[msgs.length - 1]
|
||||
if (lastMsg?.isStreaming) {
|
||||
@@ -1111,7 +1261,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!text) break
|
||||
runProducedAssistantText = true
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const last = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: null
|
||||
if (last?.role === 'assistant' && last.isStreaming) {
|
||||
last.reasoning = (last.reasoning || '') + text
|
||||
noteReasoningStart(last.id)
|
||||
@@ -1125,6 +1277,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
isStreaming: true,
|
||||
reasoning: text,
|
||||
})
|
||||
activeAssistantMessageId = newId
|
||||
noteReasoningStart(newId)
|
||||
}
|
||||
|
||||
@@ -1144,7 +1297,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'message.delta': {
|
||||
if (evt.delta) runProducedAssistantText = true
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const last = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: null
|
||||
if (last?.role === 'assistant' && last.isStreaming) {
|
||||
const prev = last.content
|
||||
const next = prev + (evt.delta || '')
|
||||
@@ -1162,6 +1317,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
timestamp: Date.now(),
|
||||
isStreaming: true,
|
||||
})
|
||||
activeAssistantMessageId = newId
|
||||
}
|
||||
|
||||
break
|
||||
@@ -1170,10 +1326,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'tool.started': {
|
||||
runHadToolActivity = true
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const last = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: msgs[msgs.length - 1]
|
||||
if (last?.isStreaming) {
|
||||
updateMessage(sid, last.id, { isStreaming: false })
|
||||
}
|
||||
activeAssistantMessageId = null
|
||||
addMessage(sid, {
|
||||
id: uid(),
|
||||
role: 'tool',
|
||||
@@ -1203,8 +1362,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
case 'run.completed': {
|
||||
const hasQueue = (evt as any).queue_remaining > 0
|
||||
if (hasQueue) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_remaining)
|
||||
} else {
|
||||
queueLengths.value.delete(sid)
|
||||
}
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastMsg = msgs[msgs.length - 1]
|
||||
const lastMsg = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: msgs[msgs.length - 1]
|
||||
if (lastMsg?.isStreaming) {
|
||||
updateMessage(sid, lastMsg.id, { isStreaming: false })
|
||||
}
|
||||
@@ -1221,7 +1388,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if ((evt as any).parsed_content !== undefined) {
|
||||
// Backend has parsed stringified array format, update last assistant message
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')
|
||||
const lastAssistant = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
: [...msgs].reverse().find(m => m.role === 'assistant')
|
||||
if (lastAssistant) {
|
||||
updateMessage(sid, lastAssistant.id, {
|
||||
content: (evt as any).parsed_content || '',
|
||||
@@ -1258,12 +1427,35 @@ export const useChatStore = defineStore('chat', () => {
|
||||
playCompletionBellIfEnabled()
|
||||
}
|
||||
|
||||
cleanup()
|
||||
// Auto-play speech for every completed assistant message
|
||||
if (autoPlaySpeechEnabled.value) {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')
|
||||
if (lastAssistant?.content) {
|
||||
setTimeout(() => {
|
||||
playMessageSpeech(lastAssistant.id, lastAssistant.content)
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasQueue) {
|
||||
cleanup()
|
||||
activeAssistantMessageId = null
|
||||
} else {
|
||||
// More runs pending — reset for next run but don't cleanup
|
||||
activeAssistantMessageId = null
|
||||
}
|
||||
updateSessionTitle(sid)
|
||||
break
|
||||
}
|
||||
|
||||
case 'run.failed': {
|
||||
const hasQueue = (evt as any).queue_remaining > 0
|
||||
if (hasQueue) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_remaining)
|
||||
} else {
|
||||
queueLengths.value.delete(sid)
|
||||
}
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastErr = msgs[msgs.length - 1]
|
||||
if (lastErr?.isStreaming) {
|
||||
@@ -1285,7 +1477,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
msgs[i] = { ...m, toolStatus: 'error' }
|
||||
}
|
||||
})
|
||||
cleanup()
|
||||
if (!hasQueue) {
|
||||
cleanup()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1316,6 +1510,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
onAbortStarted: (evt) => handleEvent(evt),
|
||||
onAbortCompleted: (evt) => handleEvent(evt),
|
||||
onUsageUpdated: (evt) => handleEvent(evt),
|
||||
onRunQueued: (evt) => handleEvent(evt),
|
||||
})
|
||||
|
||||
// No need to emit resume here — switchSession already did it.
|
||||
@@ -1343,6 +1538,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (lastMsg?.isStreaming) {
|
||||
updateMessage(sid, lastMsg.id, { isStreaming: false })
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (activeSessionId.value === sid && abortState.value?.aborting) {
|
||||
streamStates.value.delete(sid)
|
||||
serverWorking.value.delete(sid)
|
||||
setAbortState(null)
|
||||
}
|
||||
}, 20_000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,6 +1654,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
compressionState,
|
||||
abortState,
|
||||
isAborting,
|
||||
queueLengths,
|
||||
queuedUserMessages,
|
||||
removeQueuedMessage,
|
||||
isLoadingSessions,
|
||||
sessionsLoaded,
|
||||
isLoadingMessages,
|
||||
|
||||
Reference in New Issue
Block a user