diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 156e3190..8f9c07d5 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -445,6 +445,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } }; + // Primary action for send button - respects queue mode setting + const handlePrimaryAction = React.useCallback(() => { + const canQueue = hasContent && currentSessionId && sessionPhase !== 'idle'; + if (queueModeEnabled && canQueue) { + handleQueueMessage(); + } else { + void handleSubmit(); + } + }, [hasContent, currentSessionId, sessionPhase, queueModeEnabled, handleQueueMessage, handleSubmit]); + // Keep a ref to handleSubmit for auto-send effect const handleSubmitRef = React.useRef(handleSubmit); handleSubmitRef.current = handleSubmit; @@ -1075,30 +1085,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const footerGapClass = 'gap-x-1.5 gap-y-0'; const isVSCode = isVSCodeRuntime(); const footerPaddingClass = isMobile ? 'px-1.5 py-1.5' : (isVSCode ? 'px-1.5 py-1' : 'px-2.5 py-1.5'); - const footerHeightClass = isMobile ? 'h-9 w-9' : (isVSCode ? 'h-[22px] w-[22px]' : 'h-7 w-7'); + const buttonSizeClass = isMobile ? 'h-8 w-8' : (isVSCode ? 'h-5 w-5' : 'h-6 w-6'); + const sendIconSizeClass = isMobile ? 'h-4 w-4' : (isVSCode ? 'h-3.5 w-3.5' : 'h-4 w-4'); + const stopIconSizeClass = isMobile ? 'h-6 w-6' : (isVSCode ? 'h-4 w-4' : 'h-5 w-5'); const iconSizeClass = isMobile ? 'h-5 w-5' : (isVSCode ? 'h-4 w-4' : 'h-[18px] w-[18px]'); - const iconButtonBaseClass = cn( - footerHeightClass, - 'flex items-center justify-center text-muted-foreground transition-none outline-none focus:outline-none flex-shrink-0' - ); + const iconButtonBaseClass = 'flex items-center justify-center text-muted-foreground transition-none outline-none focus:outline-none flex-shrink-0'; - const stopButton = canAbort ? ( - - ) : null; - - const actionButton = ( + // Send button - respects queue mode setting + const sendButton = ( ); + // Queue button for adding message to queue while working + const queueButton = ( + + ); + + // Stop button replaces send button when working + const stopButton = ( + + ); + + // Action buttons area: either send button, or stop (+ optional queue button floating above) + const actionButtons = canAbort ? ( +
+ {hasContent && queueButton} + {stopButton} +
+ ) : ( + sendButton + ); + const attachmentMenu = ( <> = ({ onOpenSettings, scrollToBo return (
{ e.preventDefault(); handlePrimaryAction(); }} className={cn( "relative pt-0 pb-4", isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area" @@ -1327,8 +1392,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo backgroundColor: currentTheme?.colors?.surface?.subtle, }} > - {stopButton} - {} {showCommandAutocomplete && ( = ({ onOpenSettings, scrollToBo outerClassName="focus-within:ring-0" className={cn( 'min-h-[52px] resize-none border-0 px-3 rounded-b-none appearance-none hover:border-transparent bg-transparent', - isMobile ? "py-2.5" : "pt-4 pb-2", - canAbort && 'pr-10' + isMobile ? "py-2.5" : "pt-4 pb-2" )} style={{ flex: 'none', @@ -1414,7 +1476,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo
- {actionButton} + {actionButtons}
= ({ onOpenSettings, scrollToBo
- {actionButton} + {actionButtons}
)} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index c3f2cb77..03310083 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -66,6 +66,8 @@ import { useSessionStore } from '@/stores/useSessionStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useContextStore } from '@/stores/contextStore'; import { useUIStore } from '@/stores/useUIStore'; +import { useMessageQueueStore } from '@/stores/messageQueueStore'; +import { useCurrentSessionActivity } from '@/hooks/useSessionActivity'; import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore'; import { opencodeClient } from '@/lib/opencode/client'; import { useDirectoryShowHidden } from '@/lib/directoryShowHidden'; @@ -358,6 +360,9 @@ export const FilesView: React.FC = () => { const setMainTabGuard = useUIStore((state) => state.setMainTabGuard); const inputBarOffset = useUIStore((state) => state.inputBarOffset); const isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen); + const queueModeEnabled = useMessageQueueStore((state) => state.queueModeEnabled); + const addToQueue = useMessageQueueStore((state) => state.addToQueue); + const { phase: sessionPhase } = useCurrentSessionActivity(); // Global mouseup to end drag selection @@ -467,19 +472,25 @@ export const FilesView: React.FC = () => { setLineSelection(null); setActiveMainTab('chat'); - void sendMessage( - message, - effectiveProviderId, - effectiveModelId, - sessionAgent, - undefined, - undefined, - undefined, - effectiveVariant - ).catch((e) => { - console.error('Failed to send comment', e); - }); - }, [lineSelection, commentText, selectedFile, fileContent, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, extractSelectedCode, sendMessage, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]); + // Check if should queue instead of send + const canQueue = sessionPhase !== 'idle'; + if (queueModeEnabled && canQueue) { + addToQueue(currentSessionId, { content: message }); + } else { + void sendMessage( + message, + effectiveProviderId, + effectiveModelId, + sessionAgent, + undefined, + undefined, + undefined, + effectiveVariant + ).catch((e) => { + console.error('Failed to send comment', e); + }); + } + }, [lineSelection, commentText, selectedFile, fileContent, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, extractSelectedCode, sendMessage, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession, queueModeEnabled, sessionPhase, addToQueue]); const mapDirectoryEntries = React.useCallback((dirPath: string, entries: Array<{ name: string; path: string; isDirectory: boolean }>): FileNode[] => { const nodes = entries diff --git a/packages/ui/src/components/views/PierreDiffViewer.tsx b/packages/ui/src/components/views/PierreDiffViewer.tsx index c945cfb2..2be4e25b 100644 --- a/packages/ui/src/components/views/PierreDiffViewer.tsx +++ b/packages/ui/src/components/views/PierreDiffViewer.tsx @@ -15,6 +15,8 @@ import { useSessionStore } from '@/stores/useSessionStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useContextStore } from '@/stores/contextStore'; import { useUIStore } from '@/stores/useUIStore'; +import { useMessageQueueStore } from '@/stores/messageQueueStore'; +import { useCurrentSessionActivity } from '@/hooks/useSessionActivity'; import { useDeviceInfo } from '@/lib/device'; import { cn, getModifierLabel } from '@/lib/utils'; @@ -188,6 +190,9 @@ export const PierreDiffViewer: React.FC = ({ const getSessionAgentSelection = useContextStore(state => state.getSessionAgentSelection); const getAgentModelForSession = useContextStore(state => state.getAgentModelForSession); const getAgentModelVariantForSession = useContextStore(state => state.getAgentModelVariantForSession); + const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled); + const addToQueue = useMessageQueueStore(state => state.addToQueue); + const { phase: sessionPhase } = useCurrentSessionActivity(); // Update main content metrics on resize useEffect(() => { @@ -303,19 +308,25 @@ export const PierreDiffViewer: React.FC = ({ setSelection(null); setActiveMainTab('chat'); - void sendMessage( - message, - effectiveProviderId, - effectiveModelId, - sessionAgent, - undefined, - undefined, - undefined, - effectiveVariant - ).catch((e) => { - console.error('Failed to send comment', e); - }); - }, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]); + // Check if should queue instead of send + const canQueue = sessionPhase !== 'idle'; + if (queueModeEnabled && canQueue) { + addToQueue(currentSessionId, { content: message }); + } else { + void sendMessage( + message, + effectiveProviderId, + effectiveModelId, + sessionAgent, + undefined, + undefined, + undefined, + effectiveVariant + ).catch((e) => { + console.error('Failed to send comment', e); + }); + } + }, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession, queueModeEnabled, sessionPhase, addToQueue]); ensurePierreThemeRegistered(lightTheme); ensurePierreThemeRegistered(darkTheme);