diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 19a683dfe195..edac767f575c 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -359,6 +359,7 @@ export default function App() { const [extensionConfirmTitle, setExtensionConfirmTitle] = useState(''); const [isLoadingSession, setIsLoadingSession] = useState(false); const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false); + const [agentWaitingMessage, setAgentWaitingMessage] = useState(null); // Add separate state for pair chat to maintain its own conversation const [pairChat, setPairChat] = useState({ @@ -449,16 +450,19 @@ export default function App() { getExtensions, addExtension, setPairChat, + setMessage: setAgentWaitingMessage, provider: provider as string, model: model as string, }); }; - initialize().catch((error) => { - console.error('Fatal error during initialization:', error); - setFatalError(error instanceof Error ? error.message : 'Unknown error occurred'); - }); - }, [getExtensions, addExtension, read, setPairChat]); + initialize() + .then(() => setAgentWaitingMessage(null)) + .catch((error) => { + console.error('Fatal error during initialization:', error); + setFatalError(error instanceof Error ? error.message : 'Unknown error occurred'); + }); + }, [getExtensions, addExtension, read, setPairChat, setAgentWaitingMessage]); useEffect(() => { console.log('Sending reactReady signal to Electron'); @@ -838,7 +842,12 @@ export default function App() { + } @@ -864,6 +873,7 @@ export default function App() { chat={pairChat} setChat={setPairChat} contextKey={`pair-${pairChat.id}`} + agentWaitingMessage={agentWaitingMessage} key={pairChat.id} > img.filePath && !img.error && !img.isLoading) || allDroppedFiles.some((file) => !file.error && !file.isLoading)); @@ -1074,6 +1076,7 @@ export default function ChatInput({ const canSubmit = !isLoading && !isLoadingCompaction && + agentIsReady && (displayValue.trim() || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || allDroppedFiles.some((file) => !file.error && !file.isLoading)); @@ -1337,46 +1340,56 @@ export default function ChatInput({ ) : ( - + + + + + + + +

+ {isLoadingCompaction + ? 'Summarizing conversation...' + : isAnyImageLoading + ? 'Waiting for images to save...' + : isAnyDroppedFileLoading + ? 'Processing dropped files...' + : isRecording + ? 'Recording...' + : isTranscribing + ? 'Transcribing...' + : (chatContext?.agentWaitingMessage ?? 'Send')} +

+
+
)} {/* Recording/transcribing status indicator - positioned above the button row */} diff --git a/ui/desktop/src/components/InterruptionHandler.tsx b/ui/desktop/src/components/InterruptionHandler.tsx index b6e3d1f45366..2fd57df1265a 100644 --- a/ui/desktop/src/components/InterruptionHandler.tsx +++ b/ui/desktop/src/components/InterruptionHandler.tsx @@ -60,28 +60,28 @@ export const InterruptionHandler: React.FC = ({ bg: 'bg-red-50 dark:bg-red-950/20', border: 'border-red-200 dark:border-red-800/50', text: 'text-red-800 dark:text-red-200', - accent: 'text-red-600 dark:text-red-400' + accent: 'text-red-600 dark:text-red-400', }; case 'pause': return { bg: 'bg-amber-50 dark:bg-amber-950/20', border: 'border-amber-200 dark:border-amber-800/50', text: 'text-amber-800 dark:text-amber-200', - accent: 'text-amber-600 dark:text-amber-400' + accent: 'text-amber-600 dark:text-amber-400', }; case 'redirect': return { bg: 'bg-blue-50 dark:bg-blue-950/20', border: 'border-blue-200 dark:border-blue-800/50', text: 'text-blue-800 dark:text-blue-200', - accent: 'text-blue-600 dark:text-blue-400' + accent: 'text-blue-600 dark:text-blue-400', }; default: return { bg: 'bg-orange-50 dark:bg-orange-950/20', border: 'border-orange-200 dark:border-orange-800/50', text: 'text-orange-800 dark:text-orange-200', - accent: 'text-orange-600 dark:text-orange-400' + accent: 'text-orange-600 dark:text-orange-400', }; } }; @@ -98,33 +98,43 @@ export const InterruptionHandler: React.FC = ({ const getActionTitle = () => { switch (match.keyword.action) { - case 'stop': return 'Stop Processing'; - case 'pause': return 'Pause Processing'; - case 'redirect': return 'Redirect Processing'; - default: return 'Interrupt Processing'; + case 'stop': + return 'Stop Processing'; + case 'pause': + return 'Pause Processing'; + case 'redirect': + return 'Redirect Processing'; + default: + return 'Interrupt Processing'; } }; const getActionDescription = () => { switch (match.keyword.action) { - case 'stop': + case 'stop': return 'This will immediately stop the current processing and clear any queued messages.'; - case 'pause': + case 'pause': return 'This will pause the current processing. Queued messages will be preserved.'; - case 'redirect': + case 'redirect': return 'This will stop current processing and redirect to a new task.'; - default: + default: return 'This will interrupt the current processing.'; } }; return ( -
-
+
+
{/* Main card */} -
+
{/* Header */}
@@ -132,14 +142,12 @@ export const InterruptionHandler: React.FC = ({ {getIcon()}
-

- {getActionTitle()} -

-

- Detected: "{match.matchedText}" -

+

{getActionTitle()}

+

Detected: "{match.matchedText}"

-
+
{Math.round(match.confidence * 100)}% confident
@@ -149,9 +157,7 @@ export const InterruptionHandler: React.FC = ({
-

- {getActionDescription()} -

+

{getActionDescription()}

{/* Redirect input */} @@ -178,10 +184,13 @@ export const InterruptionHandler: React.FC = ({ {Math.round(match.confidence * 100)}%
-
0.8 ? 'bg-green-500' : - match.confidence > 0.6 ? 'bg-amber-500' : 'bg-red-500' + match.confidence > 0.8 + ? 'bg-green-500' + : match.confidence > 0.6 + ? 'bg-amber-500' + : 'bg-red-500' }`} style={{ width: `${match.confidence * 100}%` }} /> @@ -204,7 +213,11 @@ export const InterruptionHandler: React.FC = ({ className={`flex-1 bg-white/80 hover:bg-white dark:bg-white/10 dark:hover:bg-white/20 ${colors.text} font-medium shadow-md hover:shadow-lg transition-all duration-200`} > - {showRedirectInput ? 'Redirect' : match.keyword.action === 'stop' ? 'Stop' : 'Confirm'} + {showRedirectInput + ? 'Redirect' + : match.keyword.action === 'stop' + ? 'Stop' + : 'Confirm'}
diff --git a/ui/desktop/src/components/ui/Pill.tsx b/ui/desktop/src/components/ui/Pill.tsx index 7969d6555a46..ce4cc0ef60b3 100644 --- a/ui/desktop/src/components/ui/Pill.tsx +++ b/ui/desktop/src/components/ui/Pill.tsx @@ -22,11 +22,13 @@ export function Pill({ disabled = false, animated = false, }: PillProps) { - const baseStyles = 'inline-flex items-center justify-center rounded-full transition-all duration-300 ease-out font-medium'; - + const baseStyles = + 'inline-flex items-center justify-center rounded-full transition-all duration-300 ease-out font-medium'; + const variants = { default: 'bg-background border border-border hover:bg-muted/50', - glass: 'bg-white/10 dark:bg-black/10 backdrop-blur-xl border border-white/20 dark:border-white/10 shadow-lg shadow-black/5 dark:shadow-black/20 hover:bg-white/15 dark:hover:bg-black/15 hover:shadow-xl', + glass: + 'bg-white/10 dark:bg-black/10 backdrop-blur-xl border border-white/20 dark:border-white/10 shadow-lg shadow-black/5 dark:shadow-black/20 hover:bg-white/15 dark:hover:bg-black/15 hover:shadow-xl', solid: 'bg-background border border-border shadow-md hover:shadow-lg hover:scale-105', gradient: 'bg-gradient-to-r shadow-lg hover:shadow-xl hover:scale-105 border-0', glow: 'shadow-lg hover:shadow-xl hover:scale-105 border-0', @@ -54,9 +56,11 @@ export function Pill({ glass: 'text-red-700 dark:text-red-300 hover:text-red-800 dark:hover:text-red-200', }, purple: { - gradient: 'from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white', + gradient: + 'from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white', glow: 'bg-purple-500 hover:bg-purple-600 text-white shadow-purple-500/25 hover:shadow-purple-500/40', - glass: 'text-purple-700 dark:text-purple-300 hover:text-purple-800 dark:hover:text-purple-200', + glass: + 'text-purple-700 dark:text-purple-300 hover:text-purple-800 dark:hover:text-purple-200', }, slate: { gradient: 'from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white', @@ -73,20 +77,21 @@ export function Pill({ }; const animatedStyles = animated ? 'animate-pulse' : ''; - - const disabledStyles = disabled - ? 'opacity-50 cursor-not-allowed pointer-events-none' - : onClick - ? 'cursor-pointer hover:scale-105 active:scale-95' + + const disabledStyles = disabled + ? 'opacity-50 cursor-not-allowed pointer-events-none' + : onClick + ? 'cursor-pointer hover:scale-105 active:scale-95' : ''; - const colorStyles = variant === 'gradient' - ? colors[color].gradient - : variant === 'glow' - ? colors[color].glow - : variant === 'glass' - ? colors[color].glass - : ''; + const colorStyles = + variant === 'gradient' + ? colors[color].gradient + : variant === 'glow' + ? colors[color].glow + : variant === 'glass' + ? colors[color].glass + : ''; return (
void; // Context identification contextKey: string; // 'hub' or 'pair-{sessionId}' + agentWaitingMessage: string | null; } const ChatContext = createContext(undefined); @@ -30,12 +31,14 @@ interface ChatProviderProps { chat: ChatType; setChat: (chat: ChatType) => void; contextKey?: string; // Optional context key, defaults to 'hub' + agentWaitingMessage: string | null; } export const ChatProvider: React.FC = ({ children, chat, setChat, + agentWaitingMessage, contextKey = 'hub', }) => { const draftContext = useDraftContext(); @@ -108,6 +111,7 @@ export const ChatProvider: React.FC = ({ setDraft, clearDraft, contextKey, + agentWaitingMessage, }; return {children}; diff --git a/ui/desktop/src/utils/appInitialization.ts b/ui/desktop/src/utils/appInitialization.ts index d0fcd797c5ae..d45674b39105 100644 --- a/ui/desktop/src/utils/appInitialization.ts +++ b/ui/desktop/src/utils/appInitialization.ts @@ -14,6 +14,7 @@ interface InitializationDependencies { getExtensions?: (b: boolean) => Promise; addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void; + setMessage: (message: string | null) => void; provider: string; model: string; } @@ -22,6 +23,7 @@ export const initializeApp = async ({ getExtensions, addExtension, setPairChat, + setMessage, provider, model, }: InitializationDependencies) => { @@ -87,6 +89,7 @@ export const initializeApp = async ({ initPromises.push(costDbPromise); } + setMessage('starting extensions...'); await Promise.all(initPromises); } catch (error) { console.error('Error in system initialization:', error); diff --git a/ui/desktop/src/utils/interruptionDetector.ts b/ui/desktop/src/utils/interruptionDetector.ts index a441cdf6a812..74eb108981ca 100644 --- a/ui/desktop/src/utils/interruptionDetector.ts +++ b/ui/desktop/src/utils/interruptionDetector.ts @@ -15,32 +15,32 @@ export const INTERRUPTION_KEYWORDS: InterruptionKeyword[] = [ keyword: 'stop', variations: ['stop', 'halt', 'cease', 'quit', 'end', 'abort', 'cancel'], priority: 'high', - action: 'stop' + action: 'stop', }, { keyword: 'wait', variations: ['wait', 'hold', 'pause', 'hold on', 'wait up', 'hold up'], - priority: 'high', - action: 'pause' + priority: 'high', + action: 'pause', }, { keyword: 'no', variations: ['no', 'nope', 'nah', 'wrong', 'incorrect', 'not right'], priority: 'medium', - action: 'stop' + action: 'stop', }, { keyword: 'actually', variations: ['actually', 'instead', 'rather', 'better idea', 'change of plans'], priority: 'medium', - action: 'redirect' + action: 'redirect', }, { keyword: 'nevermind', variations: ['nevermind', 'never mind', 'forget it', 'ignore that', 'disregard'], priority: 'medium', - action: 'stop' - } + action: 'stop', + }, ]; export interface InterruptionMatch { @@ -59,7 +59,7 @@ export function detectInterruption(input: string): InterruptionMatch | null { } const normalizedInput = input.toLowerCase().trim(); - + // Check for exact matches first (highest confidence) for (const keyword of INTERRUPTION_KEYWORDS) { for (const variation of keyword.variations) { @@ -68,7 +68,7 @@ export function detectInterruption(input: string): InterruptionMatch | null { keyword, matchedText: variation, confidence: 1.0, - shouldInterrupt: true + shouldInterrupt: true, }; } } @@ -77,12 +77,15 @@ export function detectInterruption(input: string): InterruptionMatch | null { // Check for matches at the beginning of input (high confidence) for (const keyword of INTERRUPTION_KEYWORDS) { for (const variation of keyword.variations) { - if (normalizedInput.startsWith(variation + ' ') || normalizedInput.startsWith(variation + ',')) { + if ( + normalizedInput.startsWith(variation + ' ') || + normalizedInput.startsWith(variation + ',') + ) { return { keyword, matchedText: variation, confidence: 0.9, - shouldInterrupt: true + shouldInterrupt: true, }; } } @@ -97,7 +100,7 @@ export function detectInterruption(input: string): InterruptionMatch | null { keyword, matchedText: variation, confidence: 0.7, - shouldInterrupt: keyword.priority === 'high' + shouldInterrupt: keyword.priority === 'high', }; } }