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:
@@ -5,6 +5,7 @@ import MessagePart from "./message-part"
|
||||
interface MessageItemProps {
|
||||
message: Message
|
||||
messageInfo?: any
|
||||
isQueued?: boolean
|
||||
}
|
||||
|
||||
export default function MessageItem(props: MessageItemProps) {
|
||||
@@ -49,6 +50,10 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
</div>
|
||||
|
||||
<div class="message-content">
|
||||
<Show when={props.isQueued && isUser()}>
|
||||
<div class="message-queued-badge">QUEUED</div>
|
||||
</Show>
|
||||
|
||||
<Show when={errorMessage()}>
|
||||
<div class="message-error-block">⚠️ {errorMessage()}</div>
|
||||
</Show>
|
||||
|
||||
@@ -47,18 +47,29 @@ export default function MessageStream(props: MessageStreamProps) {
|
||||
const displayItems = createMemo(() => {
|
||||
const items: DisplayItem[] = []
|
||||
|
||||
let lastAssistantMessageId = ""
|
||||
for (let i = props.messages.length - 1; i >= 0; i--) {
|
||||
if (props.messages[i].type === "assistant") {
|
||||
lastAssistantMessageId = props.messages[i].id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of props.messages) {
|
||||
const messageInfo = props.messagesInfo?.get(message.id)
|
||||
const textParts = message.parts.filter((p) => p.type === "text" && !p.synthetic)
|
||||
const toolParts = message.parts.filter((p) => p.type === "tool")
|
||||
const reasoningParts = message.parts.filter((p) => p.type === "reasoning")
|
||||
|
||||
const isQueued = message.type === "user" && message.id > lastAssistantMessageId
|
||||
|
||||
if (textParts.length > 0 || reasoningParts.length > 0 || messageInfo?.error) {
|
||||
items.push({
|
||||
type: "message",
|
||||
data: {
|
||||
...message,
|
||||
parts: [...textParts, ...reasoningParts],
|
||||
isQueued,
|
||||
},
|
||||
messageInfo,
|
||||
})
|
||||
@@ -156,7 +167,7 @@ export default function MessageStream(props: MessageStreamProps) {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MessageItem message={item.data} messageInfo={item.messageInfo} />
|
||||
<MessageItem message={item.data} messageInfo={item.messageInfo} isQueued={item.data.isQueued} />
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user