feat(desktop): queue user messages while agent is busy
This commit is contained in:
parent
6e71598c2c
commit
bfe0c82e87
3 changed files with 136 additions and 11 deletions
|
|
@ -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={
|
||||
<QueuedMessageBar
|
||||
messages={queuedMessages}
|
||||
isRunning={isLoading}
|
||||
onRemove={removeQueuedMessage}
|
||||
onClear={clearQueuedMessages}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{currentMeta && currentMeta.authMethod === 'api-key' && (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="container px-4 pb-2">
|
||||
<div className="rounded-lg border bg-muted/40">
|
||||
<div className="flex items-center justify-between px-3 pt-2 pb-1">
|
||||
<span className="text-xs font-medium text-foreground/80">
|
||||
{messages.length} queued message{messages.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={onClear}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 pb-2 text-xs text-muted-foreground">{statusText}</div>
|
||||
<div className="px-2 pb-2 space-y-1">
|
||||
{messages.slice(0, 3).map((item) => (
|
||||
<div key={item.id} className="flex items-start justify-between gap-2 rounded-md bg-background/70 px-2 py-1.5">
|
||||
<div className="text-xs text-foreground/85 break-all">{item.text}</div>
|
||||
<button
|
||||
onClick={() => onRemove(item.id)}
|
||||
className="shrink-0 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{messages.length > 3 && (
|
||||
<div className="px-1 text-xs text-muted-foreground">
|
||||
+{messages.length - 3} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string | null>(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<QueuedLocalMessage[]>([])
|
||||
const [initError, setInitError] = useState<string | null>(null)
|
||||
const initRef = useRef(false)
|
||||
const offsetRef = useRef<number | null>(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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue