Copilot context fixes (#174)

* Update copilot context dynamically

* Fix agent context logic
This commit is contained in:
Akhilesh Sudhakar 2025-07-15 19:54:16 +05:30 committed by GitHub
parent 44a951c5d2
commit e735795e7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 18 deletions

View file

@ -54,12 +54,18 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
const cancelRef = useRef<any>(null); const cancelRef = useRef<any>(null);
const [statusBar, setStatusBar] = useState<any>(null); const [statusBar, setStatusBar] = useState<any>(null);
// Always use effectiveContext for the user's current selection
const effectiveContext = discardContext ? null : chatContext;
// Context locking state
const [lockedContext, setLockedContext] = useState<any>(effectiveContext);
const [pendingContext, setPendingContext] = useState<any>(effectiveContext);
const [isStreaming, setIsStreaming] = useState(false);
// Keep workflow ref up to date // Keep workflow ref up to date
workflowRef.current = workflow; workflowRef.current = workflow;
// Get the effective context based on user preference // Copilot streaming state
const effectiveContext = discardContext ? null : chatContext;
const { const {
streamingResponse, streamingResponse,
loading: loadingResponse, loading: loadingResponse,
@ -102,13 +108,16 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
setDiscardContext(false); setDiscardContext(false);
}, [chatContext]); }, [chatContext]);
function handleUserMessage(prompt: string) { // Memoized handleUserMessage for useImperativeHandle and hooks
const handleUserMessage = useCallback((prompt: string) => {
// Before starting streaming, lock the context to the current pendingContext
setLockedContext(pendingContext);
setMessages(currentMessages => [...currentMessages, { setMessages(currentMessages => [...currentMessages, {
role: 'user', role: 'user',
content: prompt content: prompt
}]); }]);
setIsLastInteracted(true); setIsLastInteracted(true);
} }, [setMessages, setIsLastInteracted, pendingContext, setLockedContext]);
// Effect for getting copilot response // Effect for getting copilot response
useEffect(() => { useEffect(() => {
@ -134,6 +143,34 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
return () => currentCancel(); return () => currentCancel();
}, [messages, responseError]); }, [messages, responseError]);
// --- CONTEXT LOCKING LOGIC ---
// Always update pendingContext to the latest effectiveContext
useEffect(() => {
setPendingContext(effectiveContext);
}, [effectiveContext]);
// Lock/unlock context based on streaming state
useEffect(() => {
if (loadingResponse) {
// Streaming started: lock context to the value at the start
setIsStreaming(true);
setLockedContext((prev: any) => prev ?? pendingContext); // lock to previous if already set, else to pending
} else {
// Streaming ended: update lockedContext to the last pendingContext
setIsStreaming(false);
setLockedContext(pendingContext);
}
}, [loadingResponse, pendingContext]);
// After streaming ends, update lockedContext live as effectiveContext changes
useEffect(() => {
if (!isStreaming) {
setLockedContext(effectiveContext);
}
// If streaming, do not update lockedContext
}, [effectiveContext, isStreaming]);
// --- END CONTEXT LOCKING LOGIC ---
const handleCopyChat = useCallback(() => { const handleCopyChat = useCallback(() => {
if (onCopyJson) { if (onCopyJson) {
onCopyJson({ onCopyJson({
@ -145,7 +182,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleCopyChat, handleCopyChat,
handleUserMessage handleUserMessage
}), [handleCopyChat]); }), [handleCopyChat, handleUserMessage]);
return ( return (
<CopilotContext.Provider value={{ workflow: workflowRef.current, dispatch }}> <CopilotContext.Provider value={{ workflow: workflowRef.current, dispatch }}>
@ -159,8 +196,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
dispatch={dispatch} dispatch={dispatch}
onStatusBarChange={status => setStatusBar({ onStatusBarChange={status => setStatusBar({
...status, ...status,
context: effectiveContext, context: lockedContext,
onCloseContext: () => setDiscardContext(true)
})} })}
/> />
</div> </div>
@ -187,7 +223,6 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
</Button> </Button>
</div> </div>
)} )}
{/* Remove the separate context label here */}
<ComposeBoxCopilot <ComposeBoxCopilot
handleUserMessage={handleUserMessage} handleUserMessage={handleUserMessage}
messages={messages} messages={messages}
@ -196,7 +231,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
shouldAutoFocus={isLastInteracted} shouldAutoFocus={isLastInteracted}
onFocus={() => setIsLastInteracted(true)} onFocus={() => setIsLastInteracted(true)}
onCancel={cancel} onCancel={cancel}
statusBar={statusBar || { context: effectiveContext, onCloseContext: () => setDiscardContext(true) }} statusBar={statusBar || { context: lockedContext }}
/> />
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Spinner } from "@heroui/react"; import { Spinner } from "@heroui/react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState, useCallback } from "react";
import { z } from "zod"; import { z } from "zod";
import { Workflow} from "@/app/lib/types/workflow_types"; import { Workflow} from "@/app/lib/types/workflow_types";
import MarkdownContent from "@/app/lib/components/markdown-content"; import MarkdownContent from "@/app/lib/components/markdown-content";
@ -218,8 +218,8 @@ function AssistantMessage({
const pendingCount = Math.max(0, totalActions - appliedCount); const pendingCount = Math.max(0, totalActions - appliedCount);
const allApplied = pendingCount === 0 && totalActions > 0; const allApplied = pendingCount === 0 && totalActions > 0;
// Apply a single action // Memoized applyAction for useCallback dependencies
const applyAction = (action: any, actionIndex: number) => { const applyAction = useCallback((action: any, actionIndex: number) => {
// Only apply, do not update appliedActions here // Only apply, do not update appliedActions here
if (action.action === 'create_new') { if (action.action === 'create_new') {
switch (action.config_type) { switch (action.config_type) {
@ -276,10 +276,10 @@ function AssistantMessage({
break; break;
} }
} }
}; }, [dispatch]);
// Apply All: batch apply all unapplied actions and update state once // Memoized handleApplyAll for useEffect dependencies
const handleApplyAll = () => { const handleApplyAll = useCallback(() => {
// Find all unapplied action indices // Find all unapplied action indices
const unapplied = cardBlocks const unapplied = cardBlocks
.filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex)) .filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex))
@ -296,7 +296,7 @@ function AssistantMessage({
unapplied.forEach(({ actionIndex }) => next.add(actionIndex)); unapplied.forEach(({ actionIndex }) => next.add(actionIndex));
return next; return next;
}); });
}; }, [cardBlocks, appliedActions, setAppliedActions, applyAction]);
// Manual single apply (from card) // Manual single apply (from card)
const handleSingleApply = (action: any, actionIndex: number) => { const handleSingleApply = (action: any, actionIndex: number) => {
@ -426,7 +426,7 @@ function AssistantMessage({
handleApplyAll, handleApplyAll,
}); });
} }
}, [allCardsLoaded, allApplied, appliedCount, pendingCount, streamingLine, completedSummary, hasPanelWarning]); }, [allCardsLoaded, allApplied, appliedCount, pendingCount, streamingLine, completedSummary, hasPanelWarning, handleApplyAll, onStatusBarChange]);
// Render all cards inline, not in a panel // Render all cards inline, not in a panel
return ( return (