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/messages/StatusIndicator.jsx +105 -18 3.1.4 → 3.2.2 View file →
@@ -1,5 +1,7 @@
1 +import { useChatStore } from '@agent/state/chat';
1 2 import { useStatusStore } from '@agent/state/status';
3 +import { hasRunComponent } from '@agent/workflows/abilities/components/run';
2 4 import { useEffect, useMemo, useState } from '@wordpress/element';
3 5 import { decodeEntities } from '@wordpress/html-entities';
4 6 import { __ } from '@wordpress/i18n';
5 7
@@ -9,46 +11,131 @@
9 11 __('Working on it...', 'extendify-local'),
10 12 __('Interpreting message...', 'extendify-local'),
11 13 __('Formulating a response...', 'extendify-local'),
12 14 __('Reviewing logic...', 'extendify-local'),
15 + __('Checking the details...', 'extendify-local'),
16 + __('Weighing the options...', 'extendify-local'),
17 + __('Putting it together...', 'extendify-local'),
18 + __('Still working on it...', 'extendify-local'),
13 19 ],
14 - 'workflow-tool-processing': __('Processing...', 'extendify-local'),
20 + 'workflow-tool-processing': label || __('Processing...', 'extendify-local'),
15 21 'tool-started': label || __('Gathering data...', 'extendify-local'),
16 22 'credits-exhausted': __('Usage limit reached', 'extendify-local'),
17 23 'credits-restored': __('Usage limit restored', 'extendify-local'),
18 24 });
19 25
26 +const beat = () => 3000 + Math.random() * 2000;
27 +
20 28 // Randomized 3-5s beat so a long wait never looks stuck or syncs across sites.
21 -const useCurrentLine = (lines) => {
22 - const [index, setIndex] = useState(0);
23 - const isList = Array.isArray(lines);
29 +// Kept across status changes, or 3s tool steps restart every beat before it lands.
30 +const useCurrentLine = (requestId, lines) => {
31 + const [tick, setTick] = useState({ requestId, count: 0, due: 0 });
32 + const current = tick.requestId === requestId;
33 + const count = current ? tick.count : 0;
34 + const due = current ? tick.due : 0;
24 35
25 - useEffect(() => setIndex(0), [lines]);
26 -
27 36 useEffect(() => {
28 - if (!isList) return;
37 + if (!lines) return;
38 + if (!due) {
39 + setTick({ requestId, count, due: Date.now() + beat() });
40 + return;
41 + }
29 42 const timer = setTimeout(
30 - () => setIndex((i) => (i + 1) % lines.length),
31 - 3000 + Math.random() * 2000,
43 + () => setTick({ requestId, count: count + 1, due: Date.now() + beat() }),
44 + Math.max(0, due - Date.now()),
32 45 );
33 46 return () => clearTimeout(timer);
34 - }, [isList, lines, index]);
47 + }, [lines, requestId, count, due]);
35 48
36 - return isList ? lines[index] : lines;
49 + if (!Array.isArray(lines)) return lines;
50 + return lines[Math.min(count, lines.length - 1)];
37 51 };
38 52
39 -export const StatusIndicator = () => {
53 +export const useStatusLine = () => {
54 + const leavingPage = useStatusStore((s) => s.leavingPage);
55 + const last = useChatStore((s) => s.messages.at(-1));
56 + const requestId = useChatStore(
57 + (s) =>
58 + s.messages.findLast(
59 + (m) => m.type === 'message' && m.details?.role === 'user',
60 + )?.id,
61 + );
62 + // A live picker shows its own waiting state; the status line would repeat it.
63 + const awaitingPicker =
64 + last?.type === 'tool' &&
65 + !('result' in (last.details ?? {})) &&
66 + (last.details?.id === 'acquire-image' || hasRunComponent(last.details?.id));
40 67 const { type, label } = useStatusStore((s) => s.statuses.at(-1)) ?? {};
41 68 const lines = useMemo(() => linesFor(label)[type], [label, type]);
42 - const text = useCurrentLine(lines);
69 + const currentLine = useCurrentLine(requestId, lines);
70 + const text = leavingPage
71 + ? __('Refreshing the page to show your changes…', 'extendify-local')
72 + : currentLine;
43 73
44 - if (!text) return null;
74 + if (!text || awaitingPicker) return null;
75 + return decodeEntities(text);
76 +};
45 77
78 +const randomWave = (startY) => {
79 + const cycles = 1.5 + Math.random();
80 + const offset = startY - 12;
81 + const amplitude = Math.max(3.5 + Math.random() * 2.5, Math.abs(offset));
82 + const rise = Math.asin(offset / amplitude);
83 + const phase = Math.random() < 0.5 ? rise : Math.PI - rise;
84 + const points = Array.from({ length: 31 }, (_, i) => {
85 + const t = i / 30;
86 + const y = 12 + amplitude * Math.sin(Math.PI * 2 * cycles * t + phase);
87 + return [3 + 18 * t, y];
88 + });
89 + return {
90 + d: `M${points.map(([x, y]) => `${x.toFixed(1)} ${y.toFixed(1)}`).join('L')}`,
91 + endY: points.at(-1)[1],
92 + };
93 +};
94 +
95 +export const StatusScribble = () => {
96 + const [wave, setWave] = useState(() => randomWave(12));
46 97 return (
47 - <div className="p-2 text-center text-xs italic text-gray-700">
98 + <svg
99 + viewBox="0 0 24 24"
100 + width="16"
101 + height="16"
102 + fill="none"
103 + aria-hidden="true"
104 + className="shrink-0 text-design-main"
105 + >
106 + <path
107 + className="status-scribble"
108 + d={wave.d}
109 + onAnimationIteration={() => setWave((w) => randomWave(w.endY))}
110 + stroke="currentColor"
111 + strokeWidth="2"
112 + strokeLinecap="round"
113 + strokeLinejoin="round"
114 + pathLength="1"
115 + />
116 + </svg>
117 + );
118 +};
119 +
120 +export const StatusIndicator = () => {
121 + const line = useStatusLine();
122 + // Every chat message clears the statuses; unmounting then cuts the icon's loop short.
123 + return (
124 + <div
125 + data-status-indicator
126 + className={
127 + line
128 + ? 'flex items-center gap-1.5 px-1 pb-1.5 text-sm italic text-gray-700'
129 + : 'invisible flex h-0 overflow-hidden'
130 + }
131 + >
132 + <StatusScribble />
48 133 {/* Reusing the node drops the new line mid-sweep. */}
49 - <span key={text} className="status-animation">
50 - {decodeEntities(text)}
51 - </span>
134 + {line ? (
135 + <span key={line} className="status-animation">
136 + {line}
137 + </span>
138 + ) : null}
52 139 </div>
53 140 );
54 141 };