diff --git a/apps/desktop/src/renderer/src/components/local-chat.tsx b/apps/desktop/src/renderer/src/components/local-chat.tsx
index 2e083111..469f0e49 100644
--- a/apps/desktop/src/renderer/src/components/local-chat.tsx
+++ b/apps/desktop/src/renderer/src/components/local-chat.tsx
@@ -6,6 +6,7 @@ import { useLocalChat } from '../hooks/use-local-chat'
import { useProviderStore } from '../stores/provider'
import { ApiKeyDialog } from './api-key-dialog'
import { OAuthDialog } from './oauth-dialog'
+import { QueuedMessageBar } from './queued-message-bar'
interface LocalChatProps {
initialPrompt?: string
@@ -25,8 +26,11 @@ export function LocalChat({ initialPrompt }: LocalChatProps) {
contextWindowTokens,
error,
pendingApprovals,
+ queuedMessages,
sendMessage,
abortGeneration,
+ removeQueuedMessage,
+ clearQueuedMessages,
loadMore,
resolveApproval,
clearError,
@@ -119,6 +123,14 @@ export function LocalChat({ initialPrompt }: LocalChatProps) {
loadMore={loadMore}
resolveApproval={resolveApproval}
errorAction={errorAction}
+ bottomSlot={
+
+ }
/>
{currentMeta && currentMeta.authMethod === 'api-key' && (
diff --git a/apps/desktop/src/renderer/src/components/queued-message-bar.tsx b/apps/desktop/src/renderer/src/components/queued-message-bar.tsx
new file mode 100644
index 00000000..32b058d3
--- /dev/null
+++ b/apps/desktop/src/renderer/src/components/queued-message-bar.tsx
@@ -0,0 +1,53 @@
+import type { QueuedLocalMessage } from '../hooks/use-local-chat'
+
+interface QueuedMessageBarProps {
+ messages: QueuedLocalMessage[]
+ isRunning: boolean
+ onRemove: (id: string) => void
+ onClear: () => void
+}
+
+export function QueuedMessageBar({ messages, isRunning, onRemove, onClear }: QueuedMessageBarProps) {
+ if (messages.length === 0) return null
+
+ const statusText = isRunning
+ ? 'Agent is running. Queued messages will send automatically.'
+ : 'Queued messages are being sent.'
+
+ return (
+
+
+
+
+ {messages.length} queued message{messages.length > 1 ? 's' : ''}
+
+
+
+
{statusText}
+
+ {messages.slice(0, 3).map((item) => (
+
+
{item.text}
+
+
+ ))}
+ {messages.length > 3 && (
+
+ +{messages.length - 3} more
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/desktop/src/renderer/src/hooks/use-local-chat.ts b/apps/desktop/src/renderer/src/hooks/use-local-chat.ts
index 6dd512d3..9a970a5b 100644
--- a/apps/desktop/src/renderer/src/hooks/use-local-chat.ts
+++ b/apps/desktop/src/renderer/src/hooks/use-local-chat.ts
@@ -9,15 +9,27 @@ import type {
} from '@multica/sdk'
import { DEFAULT_MESSAGES_LIMIT } from '@multica/sdk'
+export interface QueuedLocalMessage {
+ id: string
+ text: string
+ createdAt: number
+}
+
+function makeQueueId(): string {
+ return globalThis.crypto?.randomUUID?.() ?? `queued-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
+}
+
export function useLocalChat() {
const chat = useChat()
const chatRef = useRef(chat)
chatRef.current = chat
const [agentId, setAgentId] = useState(null)
const [isLoading, setIsLoading] = useState(false)
+ const isLoadingRef = useRef(false)
const [isLoadingHistory, setIsLoadingHistory] = useState(true)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const isLoadingMoreRef = useRef(false)
+ const [queuedMessages, setQueuedMessages] = useState([])
const [initError, setInitError] = useState(null)
const initRef = useRef(false)
const offsetRef = useRef(null)
@@ -109,17 +121,62 @@ export function useLocalChat() {
}
}, [agentId])
- const sendMessage = useCallback(
- (text: string) => {
- const trimmed = text.trim()
- if (!trimmed || !agentId) return
- chatRef.current.addUserMessage(trimmed, agentId, { type: 'local' })
- chatRef.current.setError(null)
- window.electronAPI.localChat.send(agentId, trimmed).catch(() => {})
- setIsLoading(true)
- },
- [agentId],
- )
+ useEffect(() => {
+ isLoadingRef.current = isLoading
+ }, [isLoading])
+
+ const dispatchMessageNow = useCallback((text: string) => {
+ const trimmed = text.trim()
+ if (!trimmed || !agentId) return
+ chatRef.current.addUserMessage(trimmed, agentId, { type: 'local' })
+ chatRef.current.setError(null)
+ setIsLoading(true)
+ window.electronAPI.localChat.send(agentId, trimmed)
+ .then((result) => {
+ const response = result as { ok?: boolean; error?: string } | undefined
+ if (response?.error) {
+ setIsLoading(false)
+ }
+ })
+ .catch(() => {
+ setIsLoading(false)
+ })
+ }, [agentId])
+
+ const sendMessage = useCallback((text: string) => {
+ const trimmed = text.trim()
+ if (!trimmed || !agentId) return
+
+ if (isLoadingRef.current) {
+ setQueuedMessages((prev) => [
+ ...prev,
+ {
+ id: makeQueueId(),
+ text: trimmed,
+ createdAt: Date.now(),
+ },
+ ])
+ return
+ }
+
+ dispatchMessageNow(trimmed)
+ }, [agentId, dispatchMessageNow])
+
+ const removeQueuedMessage = useCallback((id: string) => {
+ setQueuedMessages((prev) => prev.filter((item) => item.id !== id))
+ }, [])
+
+ const clearQueuedMessages = useCallback(() => {
+ setQueuedMessages([])
+ }, [])
+
+ useEffect(() => {
+ if (!agentId || isLoading || queuedMessages.length === 0) return
+ const next = queuedMessages[0]
+ if (!next) return
+ setQueuedMessages((prev) => prev.slice(1))
+ dispatchMessageNow(next.text)
+ }, [agentId, isLoading, queuedMessages, dispatchMessageNow])
const abortGeneration = useCallback(() => {
if (!agentId) return
@@ -177,8 +234,11 @@ export function useLocalChat() {
contextWindowTokens: chat.contextWindowTokens,
error: chat.error,
pendingApprovals: chat.pendingApprovals,
+ queuedMessages,
sendMessage,
abortGeneration,
+ removeQueuedMessage,
+ clearQueuedMessages,
loadMore,
resolveApproval,
clearError,