PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/Agent/components/ChatMessages.jsx +65 -12 3.1.6 → 3.2.2 View file →
@@ -1,14 +1,19 @@
1 1 import { useCanvasWorkflow } from '@agent/components/Canvas';
2 2 import { ErrorMessage } from '@agent/components/ErrorMessage';
3 3 import { AgentMessage } from '@agent/components/messages/AgentMessage';
4 4 import { ImageToolMessage } from '@agent/components/messages/ImageToolMessage';
5 -import { StatusIndicator } from '@agent/components/messages/StatusIndicator';
6 5 import { ToolReceipt } from '@agent/components/messages/ToolReceipt';
6 +import {
7 + highlightToolSteps,
8 + ToolStep,
9 +} from '@agent/components/messages/ToolStep';
7 10 import { UserMessage } from '@agent/components/messages/UserMessage';
8 11 import { WorkflowComponent } from '@agent/components/messages/WorkflowComponent';
9 12 import { WorkflowMessage } from '@agent/components/messages/WorkflowMessage';
13 +import { SavingState } from '@agent/components/SavingState';
10 14 import { ScrollDownButton } from '@agent/components/ScrollDownButton';
15 +import { cardMessageIndex } from '@agent/follow-ups/pick-next';
11 16 import { useWhenFinishedToolProps } from '@agent/hooks/useWhenFinishedToolProps';
12 17 import { useChatStore } from '@agent/state/chat';
13 18 import { useGlobalStore } from '@agent/state/global';
14 19 import { useWorkflowStore } from '@agent/state/workflows';
@@ -33,14 +38,27 @@
33 38 (window.extAgentData?.wpAbilities ?? [])
34 39 .flatMap((category) => category.abilities ?? [])
35 40 .find((ability) => ability.name === name)?.label || name;
36 41
42 +// Stored history can predate started labels; those render as plain receipts.
43 +const isStep = ({ type, details }) =>
44 + type === 'tool' &&
45 + Boolean(details?.started) &&
46 + !details.result?.error &&
47 + !hasRunComponent(details.id);
48 +
37 49 export const ChatMessages = () => {
38 50 const { open } = useGlobalStore();
39 51 const { messages } = useChatStore();
40 - const { getWorkflow } = useWorkflowStore();
52 + const {
53 + getWorkflow,
54 + whenFinishedToolProps: staged,
55 + reloadedToolProps,
56 + } = useWorkflowStore();
41 57 const workflow = getWorkflow();
42 58 const whenFinishedToolProps = useWhenFinishedToolProps();
59 + // Its preview was lost with the page, so Save would write changes nobody saw.
60 + const confirmFromReload = Boolean(staged) && staged === reloadedToolProps;
43 61 const canvasWorkflow = useCanvasWorkflow();
44 62 const whenFinishedComponent = workflow?.whenFinished?.component;
45 63 const [canScrollDown, setCanScrollDown] = useState(false);
46 64 const containerRef = useRef(null);
@@ -57,11 +75,18 @@
57 75 };
58 76
59 77 const lastId = messages.at(-1)?.id;
60 78 const lastDetails = messages.at(-1)?.details;
79 + // A run's receipt follows its tool, so the newest card is never the last message.
80 + const lastRunId = messages
81 + .toReversed()
82 + .find(
83 + (message) =>
84 + message.type === 'tool' && hasRunComponent(message.details?.id),
85 + )?.id;
86 + const cardMessageId = messages[cardMessageIndex(messages)]?.id;
61 87 const pendingTool =
62 88 messages.at(-1)?.type === 'tool' && !('result' in (lastDetails ?? {}));
63 - // Both render their own waiting state, so the shared status line doubles it.
64 89 const awaitingPicker =
65 90 pendingTool &&
66 91 (lastDetails?.id === 'acquire-image' || hasRunComponent(lastDetails?.id));
67 92
@@ -153,9 +178,10 @@
153 178 const scrollArea = c?.querySelector('#extendify-agent-chat-scroll-area');
154 179 const tool = scrollArea?.lastElementChild;
155 180 if (!c || !scrollArea || !tool) return;
156 181 const pinWhenOutOfView = () => {
157 - if (confirmScrolledFor.current === pinTarget) return;
182 + // A confirm removed on Save measures as off-screen.
183 + if (!tool.isConnected || confirmScrolledFor.current === pinTarget) return;
158 184 const cRect = c.getBoundingClientRect();
159 185 const toolRect = tool.getBoundingClientRect();
160 186 if (toolRect.top >= cRect.top && toolRect.bottom <= cRect.bottom) return;
161 187 confirmScrolledFor.current = pinTarget;
@@ -193,9 +219,9 @@
193 219 scrollArea.style.minHeight = `${offset + c.clientHeight}px`;
194 220 last.scrollIntoView({ behavior: 'smooth', block: 'start' });
195 221 }, [isUserMessage, messages]);
196 222
197 - // Chasing suggestions from far up the transcript would move the page.
223 + // Chasing a follow-up card from far up the transcript would move the page.
198 224 const lastIsWorkflow = messages.at(-1)?.type === 'workflow';
199 225 useEffect(() => {
200 226 if (!lastIsWorkflow) return;
201 227 const c = containerRef.current;
@@ -208,8 +234,13 @@
208 234 if (below > c.clientHeight * 2) return;
209 235 last.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
210 236 }, [lastIsWorkflow, messages]);
211 237
238 + // Painted up front so opening a step never shows plain JSON first.
239 + useEffect(() => {
240 + highlightToolSteps(containerRef.current);
241 + }, [messages]);
242 +
212 243 // Handles the scroll down button visibility
213 244 useLayoutEffect(() => {
214 245 const c = containerRef.current;
215 246 if (!c) return;
@@ -241,9 +272,10 @@
241 272
242 273 return (
243 274 <div
244 275 ref={containerRef}
245 - style={{ overscrollBehavior: 'contain' }}
276 + // A classic scrollbar appearing would narrow and rewrap the messages.
277 + style={{ overscrollBehavior: 'contain', scrollbarGutter: 'stable' }}
246 278 className="relative grow overflow-y-auto overflow-x-hidden p-1 pb-0 text-sm text-gray-900 md:p-2"
247 279 >
248 280 <div
249 281 id="extendify-agent-chat-scroll-area"
@@ -249,8 +281,11 @@
249 281 id="extendify-agent-chat-scroll-area"
250 282 className={ready ? '' : 'invisible pointer-events-none'}
251 283 >
252 284 {messages.map((message) => {
285 + if (isStep(message)) {
286 + return <ToolStep key={message.id} details={message.details} />;
287 + }
253 288 const freshLoad = isFreshPageLoad.current;
254 289 if (message.details?.role === 'user') {
255 290 return <UserMessage key={message.id} message={message} />;
256 291 }
@@ -264,9 +299,15 @@
264 299 />
265 300 );
266 301 }
267 302 if (message.type === 'workflow') {
268 - return <WorkflowMessage key={message.id} message={message} />;
303 + return (
304 + <WorkflowMessage
305 + key={message.id}
306 + message={message}
307 + latest={message.id === cardMessageId}
308 + />
309 + );
269 310 }
270 311 if (message.type === 'workflow-component') {
271 312 return <WorkflowComponent key={message.id} message={message} />;
272 313 }
@@ -315,8 +356,9 @@
315 356 <AbilityRun
316 357 id={message.details.id}
317 358 inputs={message.details.inputs}
318 359 result={message.details.result}
360 + live={message.id === lastRunId}
319 361 />
320 362 ) : null}
321 363 {failure ? (
322 364 <ErrorMessage>
@@ -345,16 +387,27 @@
345 387 );
346 388 }
347 389 return null;
348 390 })}
349 - {/* The tool-running status reads as stuck while the picker waits on the user. */}
350 - {awaitingPicker ? null : <StatusIndicator />}
351 391 {!workflow?.needsRedirect?.() &&
352 392 whenFinishedToolProps?.id &&
393 + !confirmFromReload &&
353 394 !canvasWorkflow &&
354 - whenFinishedComponent
355 - ? createElement(whenFinishedComponent, whenFinishedToolProps)
356 - : null}
395 + whenFinishedComponent ? (
396 + whenFinishedToolProps.processing ? (
397 + <SavingState
398 + label={
399 + whenFinishedToolProps.agentResponse?.whenFinishedTool?.labels
400 + ?.started
401 + }
402 + />
403 + ) : (
404 + createElement(whenFinishedComponent, {
405 + ...whenFinishedToolProps,
406 + live: true,
407 + })
408 + )
409 + ) : null}
357 410 {workflow?.needsRedirect?.() ? <workflow.redirectComponent /> : null}
358 411 </div>
359 412 <ScrollDownButton
360 413 canScrollDown={canScrollDown}