PluginProbe
Extendify / trunk
Extendify vtrunk
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | src/Agent/components/ChatMessages.jsx +30 -3 3.1.4 → trunk View file →
@@ -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}