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,