feat: display queued messages and allow submit while session is processing

This commit is contained in:
Zac Gaetano 2026-05-30 10:09:29 -04:00
parent 1fcd5a7880
commit 4be8d5d1d7

View file

@ -11,7 +11,7 @@ import type {
SetStateAction, SetStateAction,
TouchEvent, TouchEvent,
} from 'react'; } from 'react';
import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon } from 'lucide-react'; import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon, ClockIcon } from 'lucide-react';
import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types'; import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types';
import CommandMenu from './CommandMenu'; import CommandMenu from './CommandMenu';
import ClaudeStatus from './ClaudeStatus'; import ClaudeStatus from './ClaudeStatus';
@ -45,6 +45,11 @@ interface SlashCommand {
[key: string]: unknown; [key: string]: unknown;
} }
interface QueuedMessage {
id: string;
text: string;
}
interface ChatComposerProps { interface ChatComposerProps {
pendingPermissionRequests: PendingPermissionRequest[]; pendingPermissionRequests: PendingPermissionRequest[];
handlePermissionDecision: ( handlePermissionDecision: (
@ -101,6 +106,8 @@ interface ChatComposerProps {
placeholder: string; placeholder: string;
isTextareaExpanded: boolean; isTextareaExpanded: boolean;
sendByCtrlEnter?: boolean; sendByCtrlEnter?: boolean;
messageQueue?: QueuedMessage[];
onRemoveQueued?: (id: string) => void;
} }
export default function ChatComposer({ export default function ChatComposer({
@ -156,6 +163,8 @@ export default function ChatComposer({
placeholder, placeholder,
isTextareaExpanded, isTextareaExpanded,
sendByCtrlEnter, sendByCtrlEnter,
messageQueue = [],
onRemoveQueued,
}: ChatComposerProps) { }: ChatComposerProps) {
const { t } = useTranslation('chat'); const { t } = useTranslation('chat');
const textareaRect = textareaRef.current?.getBoundingClientRect(); const textareaRect = textareaRef.current?.getBoundingClientRect();
@ -165,12 +174,10 @@ export default function ChatComposer({
bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90, bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90,
}; };
// Detect if the AskUserQuestion interactive panel is active
const hasQuestionPanel = pendingPermissionRequests.some( const hasQuestionPanel = pendingPermissionRequests.some(
(r) => r.toolName === 'AskUserQuestion' (r) => r.toolName === 'AskUserQuestion'
); );
// Hide the thinking/status bar while any permission request is pending
const hasPendingPermissions = pendingPermissionRequests.length > 0; const hasPendingPermissions = pendingPermissionRequests.length > 0;
return ( return (
@ -194,6 +201,32 @@ export default function ChatComposer({
</div> </div>
)} )}
{/* Queued messages — shown when user sends while session is processing */}
{messageQueue.length > 0 && (
<div className="mx-auto mb-2 max-w-4xl space-y-1.5">
{messageQueue.map((q) => (
<div
key={q.id}
className="flex items-center gap-2 rounded-lg border border-border/50 bg-muted/40 px-3 py-1.5 text-sm text-muted-foreground"
>
<ClockIcon className="h-3.5 w-3.5 flex-shrink-0 text-amber-500" />
<span className="flex-1 truncate">{q.text}</span>
<span className="text-[10px] uppercase tracking-wide text-muted-foreground/60">Queued</span>
{onRemoveQueued && (
<button
type="button"
onClick={() => onRemoveQueued(q.id)}
className="rounded p-0.5 hover:bg-accent"
aria-label="Remove queued message"
>
<XIcon className="h-3 w-3" />
</button>
)}
</div>
))}
</div>
)}
{!hasQuestionPanel && <div className="relative mx-auto max-w-4xl"> {!hasQuestionPanel && <div className="relative mx-auto max-w-4xl">
{isUserScrolledUp && hasMessages && ( {isUserScrolledUp && hasMessages && (
<div className="absolute -top-10 left-0 right-0 z-10 flex justify-center"> <div className="absolute -top-10 left-0 right-0 z-10 flex justify-center">
@ -396,10 +429,12 @@ export default function ChatComposer({
input.trim() ? 'opacity-0' : 'opacity-100' input.trim() ? 'opacity-0' : 'opacity-100'
}`} }`}
> >
{sendByCtrlEnter ? t('input.hintText.ctrlEnter') : t('input.hintText.enter')} {isLoading
? t('input.hintText.queue', { defaultValue: 'Enter to queue' })
: sendByCtrlEnter ? t('input.hintText.ctrlEnter') : t('input.hintText.enter')}
</div> </div>
<PromptInputSubmit <PromptInputSubmit
disabled={!input.trim() || isLoading} disabled={!input.trim()}
className="h-10 w-10 sm:h-10 sm:w-10" className="h-10 w-10 sm:h-10 sm:w-10"
onMouseDown={(event) => { onMouseDown={(event) => {
event.preventDefault(); event.preventDefault();