Block send until extensions are ready (#4271)

This commit is contained in:
Jack Amadeo
2025-08-22 11:45:25 -04:00
committed by GitHub
parent 9d9fb0609d
commit 24b76baa55
7 changed files with 157 additions and 106 deletions
+16 -6
View File
@@ -359,6 +359,7 @@ export default function App() {
const [extensionConfirmTitle, setExtensionConfirmTitle] = useState<string>(''); const [extensionConfirmTitle, setExtensionConfirmTitle] = useState<string>('');
const [isLoadingSession, setIsLoadingSession] = useState(false); const [isLoadingSession, setIsLoadingSession] = useState(false);
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false); const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
const [agentWaitingMessage, setAgentWaitingMessage] = useState<string | null>(null);
// Add separate state for pair chat to maintain its own conversation // Add separate state for pair chat to maintain its own conversation
const [pairChat, setPairChat] = useState<ChatType>({ const [pairChat, setPairChat] = useState<ChatType>({
@@ -449,16 +450,19 @@ export default function App() {
getExtensions, getExtensions,
addExtension, addExtension,
setPairChat, setPairChat,
setMessage: setAgentWaitingMessage,
provider: provider as string, provider: provider as string,
model: model as string, model: model as string,
}); });
}; };
initialize().catch((error) => { initialize()
console.error('Fatal error during initialization:', error); .then(() => setAgentWaitingMessage(null))
setFatalError(error instanceof Error ? error.message : 'Unknown error occurred'); .catch((error) => {
}); console.error('Fatal error during initialization:', error);
}, [getExtensions, addExtension, read, setPairChat]); setFatalError(error instanceof Error ? error.message : 'Unknown error occurred');
});
}, [getExtensions, addExtension, read, setPairChat, setAgentWaitingMessage]);
useEffect(() => { useEffect(() => {
console.log('Sending reactReady signal to Electron'); console.log('Sending reactReady signal to Electron');
@@ -838,7 +842,12 @@ export default function App() {
<Route <Route
path="/" path="/"
element={ element={
<ChatProvider chat={chat} setChat={setChat} contextKey="hub"> <ChatProvider
chat={chat}
setChat={setChat}
contextKey="hub"
agentWaitingMessage={agentWaitingMessage}
>
<AppLayout setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} /> <AppLayout setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} />
</ChatProvider> </ChatProvider>
} }
@@ -864,6 +873,7 @@ export default function App() {
chat={pairChat} chat={pairChat}
setChat={setPairChat} setChat={setPairChat}
contextKey={`pair-${pairChat.id}`} contextKey={`pair-${pairChat.id}`}
agentWaitingMessage={agentWaitingMessage}
key={pairChat.id} key={pairChat.id}
> >
<PairRouteWrapper <PairRouteWrapper
+53 -40
View File
@@ -136,6 +136,7 @@ export default function ChatInput({
// Draft functionality - get chat context and global draft context // Draft functionality - get chat context and global draft context
// We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub) // We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub)
const chatContext = useChatContext(); // This should always be available now const chatContext = useChatContext(); // This should always be available now
const agentIsReady = chatContext === null || chatContext.agentWaitingMessage === null;
const draftLoadedRef = useRef(false); const draftLoadedRef = useRef(false);
// Debug logging for draft context // Debug logging for draft context
@@ -1060,6 +1061,7 @@ export default function ChatInput({
const canSubmit = const canSubmit =
!isLoading && !isLoading &&
!isLoadingCompaction && !isLoadingCompaction &&
agentIsReady &&
(displayValue.trim() || (displayValue.trim() ||
pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) ||
allDroppedFiles.some((file) => !file.error && !file.isLoading)); allDroppedFiles.some((file) => !file.error && !file.isLoading));
@@ -1074,6 +1076,7 @@ export default function ChatInput({
const canSubmit = const canSubmit =
!isLoading && !isLoading &&
!isLoadingCompaction && !isLoadingCompaction &&
agentIsReady &&
(displayValue.trim() || (displayValue.trim() ||
pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) ||
allDroppedFiles.some((file) => !file.error && !file.isLoading)); allDroppedFiles.some((file) => !file.error && !file.isLoading));
@@ -1337,46 +1340,56 @@ export default function ChatInput({
<Stop /> <Stop />
</Button> </Button>
) : ( ) : (
<Button <Tooltip>
type="submit" <TooltipTrigger asChild>
size="sm" <span>
shape="round" <Button
variant="outline" type="submit"
disabled={ size="sm"
!hasSubmittableContent || shape="round"
isAnyImageLoading || variant="outline"
isAnyDroppedFileLoading || disabled={
isRecording || !hasSubmittableContent ||
isTranscribing || isAnyImageLoading ||
isLoadingCompaction isAnyDroppedFileLoading ||
} isRecording ||
className={`rounded-full px-10 py-2 flex items-center gap-2 ${ isTranscribing ||
!hasSubmittableContent || isLoadingCompaction ||
isAnyImageLoading || !agentIsReady
isAnyDroppedFileLoading || }
isRecording || className={`rounded-full px-10 py-2 flex items-center gap-2 ${
isTranscribing || !hasSubmittableContent ||
isLoadingCompaction isAnyImageLoading ||
? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600' isAnyDroppedFileLoading ||
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer' isRecording ||
}`} isTranscribing ||
title={ isLoadingCompaction ||
isLoadingCompaction !agentIsReady
? 'Summarizing conversation...' ? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600'
: isAnyImageLoading : 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer'
? 'Waiting for images to save...' }`}
: isAnyDroppedFileLoading >
? 'Processing dropped files...' <Send className="w-4 h-4" />
: isRecording <span className="text-sm">Send</span>
? 'Recording...' </Button>
: isTranscribing </span>
? 'Transcribing...' </TooltipTrigger>
: 'Send' <TooltipContent>
} <p>
> {isLoadingCompaction
<Send className="w-4 h-4" /> ? 'Summarizing conversation...'
<span className="text-sm">Send</span> : isAnyImageLoading
</Button> ? 'Waiting for images to save...'
: isAnyDroppedFileLoading
? 'Processing dropped files...'
: isRecording
? 'Recording...'
: isTranscribing
? 'Transcribing...'
: (chatContext?.agentWaitingMessage ?? 'Send')}
</p>
</TooltipContent>
</Tooltip>
)} )}
{/* Recording/transcribing status indicator - positioned above the button row */} {/* Recording/transcribing status indicator - positioned above the button row */}
@@ -60,28 +60,28 @@ export const InterruptionHandler: React.FC<InterruptionHandlerProps> = ({
bg: 'bg-red-50 dark:bg-red-950/20', bg: 'bg-red-50 dark:bg-red-950/20',
border: 'border-red-200 dark:border-red-800/50', border: 'border-red-200 dark:border-red-800/50',
text: 'text-red-800 dark:text-red-200', 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': case 'pause':
return { return {
bg: 'bg-amber-50 dark:bg-amber-950/20', bg: 'bg-amber-50 dark:bg-amber-950/20',
border: 'border-amber-200 dark:border-amber-800/50', border: 'border-amber-200 dark:border-amber-800/50',
text: 'text-amber-800 dark:text-amber-200', 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': case 'redirect':
return { return {
bg: 'bg-blue-50 dark:bg-blue-950/20', bg: 'bg-blue-50 dark:bg-blue-950/20',
border: 'border-blue-200 dark:border-blue-800/50', border: 'border-blue-200 dark:border-blue-800/50',
text: 'text-blue-800 dark:text-blue-200', 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: default:
return { return {
bg: 'bg-orange-50 dark:bg-orange-950/20', bg: 'bg-orange-50 dark:bg-orange-950/20',
border: 'border-orange-200 dark:border-orange-800/50', border: 'border-orange-200 dark:border-orange-800/50',
text: 'text-orange-800 dark:text-orange-200', 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<InterruptionHandlerProps> = ({
const getActionTitle = () => { const getActionTitle = () => {
switch (match.keyword.action) { switch (match.keyword.action) {
case 'stop': return 'Stop Processing'; case 'stop':
case 'pause': return 'Pause Processing'; return 'Stop Processing';
case 'redirect': return 'Redirect Processing'; case 'pause':
default: return 'Interrupt Processing'; return 'Pause Processing';
case 'redirect':
return 'Redirect Processing';
default:
return 'Interrupt Processing';
} }
}; };
const getActionDescription = () => { const getActionDescription = () => {
switch (match.keyword.action) { switch (match.keyword.action) {
case 'stop': case 'stop':
return 'This will immediately stop the current processing and clear any queued messages.'; 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.'; 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.'; return 'This will stop current processing and redirect to a new task.';
default: default:
return 'This will interrupt the current processing.'; return 'This will interrupt the current processing.';
} }
}; };
return ( return (
<div className={`fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4 ${className}`}> <div
<div className={`w-full max-w-md mx-auto transition-all duration-300 ease-out ${ className={`fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4 ${className}`}
isVisible ? 'scale-100 opacity-100' : 'scale-95 opacity-0' >
}`}> <div
className={`w-full max-w-md mx-auto transition-all duration-300 ease-out ${
isVisible ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
}`}
>
{/* Main card */} {/* Main card */}
<div className={`rounded-xl border shadow-2xl backdrop-blur-xl ${colors.bg} ${colors.border}`}> <div
className={`rounded-xl border shadow-2xl backdrop-blur-xl ${colors.bg} ${colors.border}`}
>
{/* Header */} {/* Header */}
<div className="p-6 border-b border-current/10"> <div className="p-6 border-b border-current/10">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
@@ -132,14 +142,12 @@ export const InterruptionHandler: React.FC<InterruptionHandlerProps> = ({
{getIcon()} {getIcon()}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className={`text-lg font-semibold ${colors.text}`}> <h3 className={`text-lg font-semibold ${colors.text}`}>{getActionTitle()}</h3>
{getActionTitle()} <p className={`text-sm mt-1 ${colors.accent}`}>Detected: "{match.matchedText}"</p>
</h3>
<p className={`text-sm mt-1 ${colors.accent}`}>
Detected: "{match.matchedText}"
</p>
</div> </div>
<div className={`text-xs px-2 py-1 rounded-full bg-white/30 dark:bg-black/20 ${colors.text}`}> <div
className={`text-xs px-2 py-1 rounded-full bg-white/30 dark:bg-black/20 ${colors.text}`}
>
{Math.round(match.confidence * 100)}% confident {Math.round(match.confidence * 100)}% confident
</div> </div>
</div> </div>
@@ -149,9 +157,7 @@ export const InterruptionHandler: React.FC<InterruptionHandlerProps> = ({
<div className="p-6"> <div className="p-6">
<div className="flex items-start gap-3 mb-4"> <div className="flex items-start gap-3 mb-4">
<AlertCircle className={`w-4 h-4 mt-0.5 flex-shrink-0 ${colors.accent}`} /> <AlertCircle className={`w-4 h-4 mt-0.5 flex-shrink-0 ${colors.accent}`} />
<p className={`text-sm leading-relaxed ${colors.text}`}> <p className={`text-sm leading-relaxed ${colors.text}`}>{getActionDescription()}</p>
{getActionDescription()}
</p>
</div> </div>
{/* Redirect input */} {/* Redirect input */}
@@ -178,10 +184,13 @@ export const InterruptionHandler: React.FC<InterruptionHandlerProps> = ({
<span className={colors.text}>{Math.round(match.confidence * 100)}%</span> <span className={colors.text}>{Math.round(match.confidence * 100)}%</span>
</div> </div>
<div className="w-full bg-white/30 dark:bg-black/20 rounded-full h-2"> <div className="w-full bg-white/30 dark:bg-black/20 rounded-full h-2">
<div <div
className={`h-2 rounded-full transition-all duration-500 ${ className={`h-2 rounded-full transition-all duration-500 ${
match.confidence > 0.8 ? 'bg-green-500' : match.confidence > 0.8
match.confidence > 0.6 ? 'bg-amber-500' : 'bg-red-500' ? 'bg-green-500'
: match.confidence > 0.6
? 'bg-amber-500'
: 'bg-red-500'
}`} }`}
style={{ width: `${match.confidence * 100}%` }} style={{ width: `${match.confidence * 100}%` }}
/> />
@@ -204,7 +213,11 @@ export const InterruptionHandler: React.FC<InterruptionHandlerProps> = ({
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`} 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`}
> >
<Zap className="w-4 h-4 mr-2" /> <Zap className="w-4 h-4 mr-2" />
{showRedirectInput ? 'Redirect' : match.keyword.action === 'stop' ? 'Stop' : 'Confirm'} {showRedirectInput
? 'Redirect'
: match.keyword.action === 'stop'
? 'Stop'
: 'Confirm'}
</Button> </Button>
</div> </div>
</div> </div>
+22 -17
View File
@@ -22,11 +22,13 @@ export function Pill({
disabled = false, disabled = false,
animated = false, animated = false,
}: PillProps) { }: 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 = { const variants = {
default: 'bg-background border border-border hover:bg-muted/50', 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', 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', 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', 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', glass: 'text-red-700 dark:text-red-300 hover:text-red-800 dark:hover:text-red-200',
}, },
purple: { 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', 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: { slate: {
gradient: 'from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white', 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 animatedStyles = animated ? 'animate-pulse' : '';
const disabledStyles = disabled const disabledStyles = disabled
? 'opacity-50 cursor-not-allowed pointer-events-none' ? 'opacity-50 cursor-not-allowed pointer-events-none'
: onClick : onClick
? 'cursor-pointer hover:scale-105 active:scale-95' ? 'cursor-pointer hover:scale-105 active:scale-95'
: ''; : '';
const colorStyles = variant === 'gradient' const colorStyles =
? colors[color].gradient variant === 'gradient'
: variant === 'glow' ? colors[color].gradient
? colors[color].glow : variant === 'glow'
: variant === 'glass' ? colors[color].glow
? colors[color].glass : variant === 'glass'
: ''; ? colors[color].glass
: '';
return ( return (
<div <div
+4
View File
@@ -21,6 +21,7 @@ interface ChatContextType {
clearDraft: () => void; clearDraft: () => void;
// Context identification // Context identification
contextKey: string; // 'hub' or 'pair-{sessionId}' contextKey: string; // 'hub' or 'pair-{sessionId}'
agentWaitingMessage: string | null;
} }
const ChatContext = createContext<ChatContextType | undefined>(undefined); const ChatContext = createContext<ChatContextType | undefined>(undefined);
@@ -30,12 +31,14 @@ interface ChatProviderProps {
chat: ChatType; chat: ChatType;
setChat: (chat: ChatType) => void; setChat: (chat: ChatType) => void;
contextKey?: string; // Optional context key, defaults to 'hub' contextKey?: string; // Optional context key, defaults to 'hub'
agentWaitingMessage: string | null;
} }
export const ChatProvider: React.FC<ChatProviderProps> = ({ export const ChatProvider: React.FC<ChatProviderProps> = ({
children, children,
chat, chat,
setChat, setChat,
agentWaitingMessage,
contextKey = 'hub', contextKey = 'hub',
}) => { }) => {
const draftContext = useDraftContext(); const draftContext = useDraftContext();
@@ -108,6 +111,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
setDraft, setDraft,
clearDraft, clearDraft,
contextKey, contextKey,
agentWaitingMessage,
}; };
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>; return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
@@ -14,6 +14,7 @@ interface InitializationDependencies {
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>; getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>; addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void; setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void;
setMessage: (message: string | null) => void;
provider: string; provider: string;
model: string; model: string;
} }
@@ -22,6 +23,7 @@ export const initializeApp = async ({
getExtensions, getExtensions,
addExtension, addExtension,
setPairChat, setPairChat,
setMessage,
provider, provider,
model, model,
}: InitializationDependencies) => { }: InitializationDependencies) => {
@@ -87,6 +89,7 @@ export const initializeApp = async ({
initPromises.push(costDbPromise); initPromises.push(costDbPromise);
} }
setMessage('starting extensions...');
await Promise.all(initPromises); await Promise.all(initPromises);
} catch (error) { } catch (error) {
console.error('Error in system initialization:', error); console.error('Error in system initialization:', error);
+15 -12
View File
@@ -15,32 +15,32 @@ export const INTERRUPTION_KEYWORDS: InterruptionKeyword[] = [
keyword: 'stop', keyword: 'stop',
variations: ['stop', 'halt', 'cease', 'quit', 'end', 'abort', 'cancel'], variations: ['stop', 'halt', 'cease', 'quit', 'end', 'abort', 'cancel'],
priority: 'high', priority: 'high',
action: 'stop' action: 'stop',
}, },
{ {
keyword: 'wait', keyword: 'wait',
variations: ['wait', 'hold', 'pause', 'hold on', 'wait up', 'hold up'], variations: ['wait', 'hold', 'pause', 'hold on', 'wait up', 'hold up'],
priority: 'high', priority: 'high',
action: 'pause' action: 'pause',
}, },
{ {
keyword: 'no', keyword: 'no',
variations: ['no', 'nope', 'nah', 'wrong', 'incorrect', 'not right'], variations: ['no', 'nope', 'nah', 'wrong', 'incorrect', 'not right'],
priority: 'medium', priority: 'medium',
action: 'stop' action: 'stop',
}, },
{ {
keyword: 'actually', keyword: 'actually',
variations: ['actually', 'instead', 'rather', 'better idea', 'change of plans'], variations: ['actually', 'instead', 'rather', 'better idea', 'change of plans'],
priority: 'medium', priority: 'medium',
action: 'redirect' action: 'redirect',
}, },
{ {
keyword: 'nevermind', keyword: 'nevermind',
variations: ['nevermind', 'never mind', 'forget it', 'ignore that', 'disregard'], variations: ['nevermind', 'never mind', 'forget it', 'ignore that', 'disregard'],
priority: 'medium', priority: 'medium',
action: 'stop' action: 'stop',
} },
]; ];
export interface InterruptionMatch { export interface InterruptionMatch {
@@ -59,7 +59,7 @@ export function detectInterruption(input: string): InterruptionMatch | null {
} }
const normalizedInput = input.toLowerCase().trim(); const normalizedInput = input.toLowerCase().trim();
// Check for exact matches first (highest confidence) // Check for exact matches first (highest confidence)
for (const keyword of INTERRUPTION_KEYWORDS) { for (const keyword of INTERRUPTION_KEYWORDS) {
for (const variation of keyword.variations) { for (const variation of keyword.variations) {
@@ -68,7 +68,7 @@ export function detectInterruption(input: string): InterruptionMatch | null {
keyword, keyword,
matchedText: variation, matchedText: variation,
confidence: 1.0, 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) // Check for matches at the beginning of input (high confidence)
for (const keyword of INTERRUPTION_KEYWORDS) { for (const keyword of INTERRUPTION_KEYWORDS) {
for (const variation of keyword.variations) { for (const variation of keyword.variations) {
if (normalizedInput.startsWith(variation + ' ') || normalizedInput.startsWith(variation + ',')) { if (
normalizedInput.startsWith(variation + ' ') ||
normalizedInput.startsWith(variation + ',')
) {
return { return {
keyword, keyword,
matchedText: variation, matchedText: variation,
confidence: 0.9, confidence: 0.9,
shouldInterrupt: true shouldInterrupt: true,
}; };
} }
} }
@@ -97,7 +100,7 @@ export function detectInterruption(input: string): InterruptionMatch | null {
keyword, keyword,
matchedText: variation, matchedText: variation,
confidence: 0.7, confidence: 0.7,
shouldInterrupt: keyword.priority === 'high' shouldInterrupt: keyword.priority === 'high',
}; };
} }
} }