| @@ -1,4 +1,5 @@ | ||
| 1 | +import { useCanvasWorkflow } from '@agent/components/Canvas'; | |
| 1 | 2 | import { ErrorMessage } from '@agent/components/ErrorMessage'; |
| 2 | 3 | import { AgentMessage } from '@agent/components/messages/AgentMessage'; |
| 3 | 4 | import { ImageToolMessage } from '@agent/components/messages/ImageToolMessage'; |
| 4 | 5 | import { StatusIndicator } from '@agent/components/messages/StatusIndicator'; |
| @@ -38,8 +39,9 @@ | ||
| 38 | 39 | const { messages } = useChatStore(); |
| 39 | 40 | const { getWorkflow } = useWorkflowStore(); |
| 40 | 41 | const workflow = getWorkflow(); |
| 41 | 42 | const whenFinishedToolProps = useWhenFinishedToolProps(); |
| 43 | + const canvasWorkflow = useCanvasWorkflow(); | |
| 42 | 44 | const whenFinishedComponent = workflow?.whenFinished?.component; |
| 43 | 45 | const [canScrollDown, setCanScrollDown] = useState(false); |
| 44 | 46 | const containerRef = useRef(null); |
| 45 | 47 | const isFreshPageLoad = useRef(true); |
| @@ -45,8 +47,15 @@ | ||
| 45 | 47 | const isFreshPageLoad = useRef(true); |
| 46 | 48 | const [ready, setReady] = useState(false); |
| 47 | 49 | const userScrolledAway = useRef(false); |
| 48 | 50 | const confirmScrolledFor = useRef(null); |
| 51 | + // Remounting into another layout would otherwise animate the whole backlog past. | |
| 52 | + const settling = useRef(true); | |
| 53 | + const behavior = () => { | |
| 54 | + if (!settling.current) return 'smooth'; | |
| 55 | + settling.current = false; | |
| 56 | + return 'auto'; | |
| 57 | + }; | |
| 49 | 58 | |
| 50 | 59 | const lastId = messages.at(-1)?.id; |
| 51 | 60 | const lastDetails = messages.at(-1)?.details; |
| 52 | 61 | const pendingTool = |
| @@ -59,9 +68,10 @@ | ||
| 59 | 68 | // If last message is a user message, move it to the top |
| 60 | 69 | const isUserMessage = messages.at(-1)?.details?.role === 'user'; |
| 61 | 70 | |
| 62 | 71 | useEffect(() => { |
| 63 | - if (!containerRef.current || !open) return; | |
| 72 | + // The frontend agent shows the chat even while the store says closed. | |
| 73 | + if (!containerRef.current) return; | |
| 64 | 74 | if (!isFreshPageLoad.current) return; |
| 65 | 75 | isFreshPageLoad.current = false; |
| 66 | 76 | // Scroll to the bottom of the chat container on load |
| 67 | 77 | const c = containerRef.current; |
| @@ -74,11 +84,14 @@ | ||
| 74 | 84 | last?.scrollIntoView({ behavior: 'auto', block: 'start' }); |
| 75 | 85 | setReady(true); |
| 76 | 86 | }); |
| 77 | 87 | }); |
| 88 | + // A hidden tab suspends animation frames, leaving the list invisible. | |
| 89 | + const fallback = setTimeout(() => setReady(true), 500); | |
| 78 | 90 | return () => { |
| 79 | 91 | cancelAnimationFrame(id); |
| 80 | 92 | cancelAnimationFrame(id2); |
| 93 | + clearTimeout(fallback); | |
| 81 | 94 | isFreshPageLoad.current = true; |
| 82 | 95 | setReady(false); |
| 83 | 96 | }; |
| 84 | 97 | }, [open]); |
| @@ -123,9 +136,9 @@ | ||
| 123 | 136 | // block:'end' scrolls UP for already-visible content — only reveal overflow. |
| 124 | 137 | const overflows = |
| 125 | 138 | last.getBoundingClientRect().bottom > c.getBoundingClientRect().bottom; |
| 126 | 139 | if (!overflows) return; |
| 127 | - last.scrollIntoView({ behavior: 'smooth', block: 'end' }); | |
| 140 | + last.scrollIntoView({ behavior: behavior(), block: 'end' }); | |
| 128 | 141 | }); |
| 129 | 142 | return () => cancelAnimationFrame(id); |
| 130 | 143 | }, [ready, isUserMessage, messages, pinTarget]); |
| 131 | 144 | |
| @@ -153,9 +166,9 @@ | ||
| 153 | 166 | const offset = |
| 154 | 167 | target.getBoundingClientRect().top - |
| 155 | 168 | scrollArea.getBoundingClientRect().top; |
| 156 | 169 | scrollArea.style.minHeight = `${offset + c.clientHeight}px`; |
| 157 | - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| 170 | + target.scrollIntoView({ behavior: behavior(), block: 'start' }); | |
| 158 | 171 | }; |
| 159 | 172 | pinWhenOutOfView(); |
| 160 | 173 | // The confirm grows while its preview loads and can leave the viewport. |
| 161 | 174 | const observer = new ResizeObserver(pinWhenOutOfView); |
| @@ -256,8 +269,21 @@ | ||
| 256 | 269 | } |
| 257 | 270 | if (message.type === 'workflow-component') { |
| 258 | 271 | return <WorkflowComponent key={message.id} message={message} />; |
| 259 | 272 | } |
| 273 | + if (message.type === 'canvas-notice') { | |
| 274 | + return ( | |
| 275 | + <ToolReceipt key={message.id}> | |
| 276 | + { | |
| 277 | + // translators: Shown in the agent chat when the user asks for something the open canvas cannot do. Canvas is the panel open on screen beside the chat. | |
| 278 | + __( | |
| 279 | + 'Canvas interactions are limited. When finished, use the button in the top corner to exit.', | |
| 280 | + 'extendify-local', | |
| 281 | + ) | |
| 282 | + } | |
| 283 | + </ToolReceipt> | |
| 284 | + ); | |
| 285 | + } | |
| 260 | 286 | if ( |
| 261 | 287 | message.type === 'tool' && |
| 262 | 288 | message.details?.id === 'acquire-image' |
| 263 | 289 | ) { |
| @@ -323,8 +349,9 @@ | ||
| 323 | 349 | {/* The tool-running status reads as stuck while the picker waits on the user. */} |
| 324 | 350 | {awaitingPicker ? null : <StatusIndicator />} |
| 325 | 351 | {!workflow?.needsRedirect?.() && |
| 326 | 352 | whenFinishedToolProps?.id && |
| 353 | + !canvasWorkflow && | |
| 327 | 354 | whenFinishedComponent |
| 328 | 355 | ? createElement(whenFinishedComponent, whenFinishedToolProps) |
| 329 | 356 | : null} |
| 330 | 357 | {workflow?.needsRedirect?.() ? <workflow.redirectComponent /> : null} |