Enable command queueing and fix message duplication issues

- Remove sending() state blocking to allow queueing multiple messages
- Add optimistic message creation with temp IDs for immediate UI feedback
- Implement QUEUED badge display matching TUI behavior (accent color, bold)
- Compute queued state: user message ID > last assistant message ID
- Clear prompt input immediately before async send for better UX
- Fix duplicate messages by properly finding and replacing temp messages
- Fix duplicate parts by clearing optimistic parts when real message arrives
- Fix state mutation bugs in message.part.updated handler (immutable updates)
- Fix state mutation bugs in message.updated handler (immutable updates)

Matches TUI implementation for consistent user experience across clients.
This commit is contained in:
Shantur Rathore
2025-10-24 16:38:42 +01:00
parent e3bc947195
commit 7be4248e20
5 changed files with 149 additions and 73 deletions

View File

@@ -26,7 +26,6 @@ interface PromptInputProps {
export default function PromptInput(props: PromptInputProps) {
const [prompt, setPrompt] = createSignal("")
const [sending, setSending] = createSignal(false)
const [history, setHistory] = createSignal<string[]>([])
const [historyIndex, setHistoryIndex] = createSignal(-1)
const [isFocused, setIsFocused] = createSignal(false)
@@ -396,9 +395,18 @@ export default function PromptInput(props: PromptInputProps) {
async function handleSend() {
const text = prompt().trim()
const currentAttachments = attachments()
if (!text || sending() || props.disabled) return
if (!text || props.disabled) return
setPrompt("")
clearAttachments(props.instanceId, props.sessionId)
setIgnoredAtPositions(new Set<number>())
setPasteCount(0)
setImageCount(0)
if (textareaRef) {
textareaRef.style.height = "auto"
}
setSending(true)
try {
await addToHistory(props.instanceFolder, text)
@@ -407,20 +415,10 @@ export default function PromptInput(props: PromptInputProps) {
setHistoryIndex(-1)
await props.onSend(text, currentAttachments)
setPrompt("")
clearAttachments(props.instanceId, props.sessionId)
setIgnoredAtPositions(new Set<number>())
setPasteCount(0)
setImageCount(0)
if (textareaRef) {
textareaRef.style.height = "auto"
}
} catch (error) {
console.error("Failed to send message:", error)
alert("Failed to send message: " + (error instanceof Error ? error.message : String(error)))
} finally {
setSending(false)
textareaRef?.focus()
}
}
@@ -612,7 +610,7 @@ export default function PromptInput(props: PromptInputProps) {
textareaRef?.focus()
}
const canSend = () => (prompt().trim().length > 0 || attachments().length > 0) && !sending() && !props.disabled
const canSend = () => (prompt().trim().length > 0 || attachments().length > 0) && !props.disabled
const instance = () => getActiveInstance()
@@ -720,16 +718,14 @@ export default function PromptInput(props: PromptInputProps) {
onPaste={handlePaste}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
disabled={sending() || props.disabled}
disabled={props.disabled}
rows={1}
style={attachments().length > 0 ? { "padding-top": "8px" } : {}}
/>
</div>
<button class="send-button" onClick={handleSend} disabled={!canSend()} aria-label="Send message">
<Show when={sending()} fallback={<span class="send-icon"></span>}>
<span class="spinner-small" />
</Show>
<span class="send-icon"></span>
</button>
</div>
<div class="prompt-input-hints">