@nexface/tools 0.1.10-alpha.0 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,190 +1,391 @@
1
1
  import { CursorIcon as Cursor } from "@phosphor-icons/react/Cursor";
2
2
  import { SparkleIcon as Sparkle } from "@phosphor-icons/react/Sparkle";
3
3
  import * as React from "react";
4
- import { useCallback, useLayoutEffect, useEffect, useRef, useState } from "react";
5
- import { createPortal } from "react-dom";
6
- import { PageCommandError } from "./browser/locator.js";
4
+ import { useLayoutEffect, useEffect, useRef, useState } from "react";
5
+ import { createPortal, flushSync } from "react-dom";
7
6
  import { lockOperationSurface } from "./operation-lock.js";
8
- const center = (rect) => ({ left: rect.left + rect.width / 2, top: rect.top + rect.height / 2 });
9
- const distance = (a, b) => Math.hypot(a.left - b.left, a.top - b.top);
10
- const interrupted = () => new PageCommandError({ code: "EXECUTION_CANCELLED", message: "Page action presentation was interrupted." });
11
- function presentPointer(pointer, target, presentation) {
12
- let finished = false;
13
- const finish = (error) => {
14
- if (finished)
15
- return;
16
- finished = true;
17
- clearTimeout(timer);
18
- pointer.removeEventListener("transitionend", onTransition);
19
- presentation.signal?.removeEventListener("abort", onAbort);
20
- presentation.finish(error);
21
- };
22
- const reached = () => pointer.isConnected && target.isConnected && distance(pointer.getBoundingClientRect(), center(target.getBoundingClientRect())) < 2;
23
- const onTransition = (event) => {
24
- if (event.target === pointer && event.propertyName === "transform" && reached())
25
- finish();
26
- };
27
- const onAbort = () => finish(interrupted());
28
- const timer = setTimeout(() => reached() ? finish() : finish(new PageCommandError({
29
- code: "ACTION_TIMEOUT", message: "Page action pointer did not reach its target in time.",
30
- })), 750);
31
- pointer.addEventListener("transitionend", onTransition);
32
- presentation.signal?.addEventListener("abort", onAbort, { once: true });
33
- queueMicrotask(() => { if (presentation.signal?.aborted)
34
- onAbort();
35
- else if (reached())
36
- finish(); });
37
- return () => finish(interrupted());
7
+ const BROWSER_TOOL_NAMES = new Set(["nxf_getSnapshot", "nxf_executeAction", "nxf_applyAction"]);
8
+ const CENTER_BROWSER_TOOL_NAMES = new Set(["nxf_getSnapshot", "nxf_executeAction"]);
9
+ const POINTER_EXIT_DURATION_MS = 120;
10
+ const CONTROLLER_EXIT_DURATION_MS = 300;
11
+ function operationProgressLabel(operation) {
12
+ const progressLabel = operation.progressLabel?.trim().replace(/\s+/g, " ");
13
+ if (progressLabel)
14
+ return progressLabel.slice(0, 48);
15
+ return BROWSER_TOOL_NAMES.has(operation.name)
16
+ ? "正在浏览网页"
17
+ : "正在处理你的请求";
18
+ }
19
+ function operationEffect(operation) {
20
+ return operation.effect ?? "action";
38
21
  }
39
- function FeedbackLabel({ feedback }) {
40
- return React.createElement("span", { className: "agent-operation__pointer-label", "data-browser-tool": feedback.centered ? "true" : undefined, "data-operation-effect": feedback.effect, title: feedback.label },
41
- feedback.effect === "processing" ? React.createElement("span", { className: "agent-operation__pointer-processing" }) : null,
42
- feedback.effect === "read" ? React.createElement("span", { className: "agent-operation__pointer-read-scan" },
43
- React.createElement("i", null),
44
- React.createElement("i", null),
45
- React.createElement("i", null),
46
- React.createElement("i", null),
47
- React.createElement("i", null)) : null,
48
- feedback.effect === "navigate" ? React.createElement("span", { className: "agent-operation__pointer-navigation-trail" }) : null,
49
- React.createElement("span", { className: "agent-operation__pointer-label-text" }, feedback.label));
22
+ function normalizedLabel(label) {
23
+ const normalized = label?.trim().replace(/\s+/g, " ");
24
+ return normalized || undefined;
50
25
  }
51
- function FeedbackIndicator({ feedback, position }) {
52
- const [lastPosition, setLastPosition] = useState(position ?? feedback.position);
53
- useLayoutEffect(() => { if (position)
54
- setLastPosition(position); }, [position?.left, position?.top]);
55
- const current = position ?? lastPosition;
56
- return current ? React.createElement("div", { "aria-hidden": true, className: "agent-operation__pointer-effect", "data-active": "true", "data-operation-effect": feedback.effect, style: { "--agent-operation-pointer-duration": "0ms", "--agent-operation-pointer-x": `${current.left}px`,
57
- "--agent-operation-pointer-y": `${current.top}px` } },
58
- React.createElement(FeedbackLabel, { feedback: feedback })) : null;
26
+ function operationTargetLabel(operation, target) {
27
+ const label = target?.label;
28
+ if (!label || label === operation.name)
29
+ return undefined;
30
+ const rawNamePrefix = `${operation.name} · `;
31
+ return label.startsWith(rawNamePrefix) ? label.slice(rawNamePrefix.length) : label;
59
32
  }
60
- function AgentPointer({ active, feedback, targetPosition, surfaceCenter }) {
61
- const pointer = useRef(null);
33
+ function pointerMoveDuration(from, to) {
34
+ const distance = Math.hypot(to.left - from.left, to.top - from.top);
35
+ if (distance < 2)
36
+ return 0;
37
+ return Math.round(Math.min(360, Math.max(180, 180 + distance * 0.18)));
38
+ }
39
+ function pointerReachedTarget(element, target) {
40
+ const rect = element.getBoundingClientRect();
41
+ return Math.hypot(rect.left - target.left, rect.top - target.top) < 2;
42
+ }
43
+ function AgentPointer({ active, initialPosition, label, labelActive, operation, targetElement, targetPosition, visible, onPointerReady }) {
44
+ const pointerRef = useRef(null);
45
+ const activeOperationRef = useRef(undefined);
46
+ const activeTargetRef = useRef(undefined);
47
+ const exitTimerRef = useRef(undefined);
48
+ const movementTimerRef = useRef(undefined);
49
+ const motionRevisionRef = useRef(0);
50
+ const latestActiveRef = useRef(active);
51
+ const latestOperationRef = useRef(operation);
62
52
  const [motion, setMotion] = useState();
63
- const action = feedback?.effect === "action" && (!feedback.requiresTarget || Boolean(targetPosition));
64
- useLayoutEffect(() => {
65
- if (!active) {
66
- setMotion(undefined);
53
+ latestActiveRef.current = active;
54
+ latestOperationRef.current = operation;
55
+ const clearExitTimer = () => {
56
+ if (!exitTimerRef.current)
57
+ return;
58
+ clearTimeout(exitTimerRef.current);
59
+ exitTimerRef.current = undefined;
60
+ };
61
+ const clearMovementTimer = () => {
62
+ if (!movementTimerRef.current)
63
+ return;
64
+ clearTimeout(movementTimerRef.current);
65
+ movementTimerRef.current = undefined;
66
+ };
67
+ const finishMovement = (revision, toolCallId) => {
68
+ const latestOperation = latestOperationRef.current;
69
+ if (!latestActiveRef.current
70
+ || motionRevisionRef.current !== revision
71
+ || latestOperation.toolCallId !== toolCallId)
72
+ return;
73
+ clearMovementTimer();
74
+ const settle = () => setMotion((current) => current?.revision === revision ? { ...current, phase: "resting" } : current);
75
+ if (latestOperation.name !== "nxf_applyAction" || !onPointerReady) {
76
+ settle();
67
77
  return;
68
78
  }
69
- if (!action || !targetPosition) {
70
- if (action && !feedback?.requiresTarget && motion && pointer.current) {
71
- const rect = pointer.current.getBoundingClientRect();
72
- if (distance(rect, motion.position) >= 1)
73
- setMotion({ ...motion, position: rect, durationMs: 0 });
74
- }
79
+ queueMicrotask(() => {
80
+ if (!latestActiveRef.current || motionRevisionRef.current !== revision || latestOperationRef.current.toolCallId !== toolCallId)
81
+ return;
82
+ flushSync(settle);
83
+ requestAnimationFrame(() => requestAnimationFrame(() => {
84
+ if (latestActiveRef.current && motionRevisionRef.current === revision
85
+ && latestOperationRef.current.toolCallId === toolCallId && activeTargetRef.current?.isConnected)
86
+ onPointerReady(toolCallId);
87
+ }));
88
+ });
89
+ };
90
+ useEffect(() => () => {
91
+ clearExitTimer();
92
+ clearMovementTimer();
93
+ }, []);
94
+ useEffect(() => {
95
+ if (!visible || !initialPosition) {
96
+ if (!motion || motion.phase === "hidden")
97
+ return;
98
+ activeOperationRef.current = undefined;
99
+ activeTargetRef.current = undefined;
100
+ clearMovementTimer();
101
+ const revision = ++motionRevisionRef.current;
102
+ setMotion((current) => current
103
+ ? { ...current, durationMs: POINTER_EXIT_DURATION_MS, phase: "hidden", revision }
104
+ : current);
105
+ clearExitTimer();
106
+ exitTimerRef.current = setTimeout(() => {
107
+ if (motionRevisionRef.current === revision)
108
+ setMotion(undefined);
109
+ }, POINTER_EXIT_DURATION_MS);
75
110
  return;
76
111
  }
112
+ clearExitTimer();
77
113
  if (!motion) {
78
- setMotion({ position: feedback?.persistentCursor && surfaceCenter ? surfaceCenter : targetPosition,
79
- durationMs: 0, persistent: Boolean(feedback?.persistentCursor) });
114
+ activeOperationRef.current = operation.toolCallId;
115
+ activeTargetRef.current = undefined;
116
+ const revision = ++motionRevisionRef.current;
117
+ setMotion({
118
+ durationMs: 0,
119
+ phase: "resting",
120
+ position: initialPosition,
121
+ revision,
122
+ toolCallId: operation.toolCallId,
123
+ });
80
124
  return;
81
125
  }
82
- if (distance(motion.position, targetPosition) < 1) {
83
- if (feedback?.persistentCursor && !motion.persistent)
84
- setMotion({ ...motion, persistent: true });
126
+ if (!active) {
127
+ activeOperationRef.current = undefined;
128
+ activeTargetRef.current = undefined;
129
+ clearMovementTimer();
130
+ if (motion.phase !== "resting") {
131
+ const revision = ++motionRevisionRef.current;
132
+ setMotion((current) => current
133
+ ? { ...current, phase: "resting", revision }
134
+ : current);
135
+ }
85
136
  return;
86
137
  }
87
- const from = pointer.current?.getBoundingClientRect() ?? motion.position;
88
- const durationMs = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0
89
- : Math.round(Math.min(360, Math.max(180, 180 + distance(from, targetPosition) * 0.18)));
90
- setMotion({ position: targetPosition, durationMs, persistent: motion.persistent || Boolean(feedback?.persistentCursor) });
91
- }, [active, action, feedback?.id, targetPosition?.left, targetPosition?.top, motion]);
92
- const show = Boolean(active && motion && (motion.persistent || action));
93
- useLayoutEffect(() => {
94
- if (!feedback?.presentation)
138
+ const operationChanged = activeOperationRef.current !== operation.toolCallId;
139
+ if (operationChanged) {
140
+ activeOperationRef.current = operation.toolCallId;
141
+ activeTargetRef.current = undefined;
142
+ clearMovementTimer();
143
+ }
144
+ else if (motion.phase === "hidden") {
145
+ setMotion((current) => current
146
+ ? { ...current, durationMs: 0, phase: "resting" }
147
+ : current);
148
+ }
149
+ if (!targetElement || !targetPosition) {
150
+ const positionChanged = CENTER_BROWSER_TOOL_NAMES.has(operation.name)
151
+ && (motion.position.left !== initialPosition.left || motion.position.top !== initialPosition.top);
152
+ if (operationChanged || activeTargetRef.current || positionChanged) {
153
+ activeTargetRef.current = undefined;
154
+ clearMovementTimer();
155
+ const revision = ++motionRevisionRef.current;
156
+ const pointerRect = pointerRef.current?.getBoundingClientRect();
157
+ setMotion((current) => current
158
+ ? {
159
+ ...current,
160
+ durationMs: 0,
161
+ phase: "resting",
162
+ position: operationChanged || positionChanged ? initialPosition : pointerRect
163
+ ? { left: pointerRect.left, top: pointerRect.top }
164
+ : current.position,
165
+ revision,
166
+ toolCallId: operation.toolCallId,
167
+ }
168
+ : current);
169
+ }
95
170
  return;
96
- if (!active || !feedback.target?.isConnected || !targetPosition) {
97
- feedback.presentation.finish(interrupted());
171
+ }
172
+ const targetChanged = operationChanged || activeTargetRef.current !== targetElement;
173
+ if (!targetChanged) {
174
+ if (motion.position.left === targetPosition.left && motion.position.top === targetPosition.top)
175
+ return;
176
+ clearMovementTimer();
177
+ const revision = ++motionRevisionRef.current;
178
+ const durationMs = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0 : 180;
179
+ setMotion((current) => current
180
+ ? { ...current, durationMs, phase: "moving", position: targetPosition, revision }
181
+ : current);
182
+ if (durationMs === 0)
183
+ finishMovement(revision, operation.toolCallId);
184
+ else
185
+ movementTimerRef.current = setTimeout(() => finishMovement(revision, operation.toolCallId), durationMs + 50);
98
186
  return;
99
187
  }
100
- if (show && pointer.current)
101
- return presentPointer(pointer.current, feedback.target, feedback.presentation);
102
- }, [active, show, feedback?.presentation, feedback?.target, Boolean(targetPosition)]);
103
- return show && motion ? React.createElement("div", { "aria-hidden": true, ref: pointer, className: "agent-operation__pointer-effect", "data-active": action, "data-operation-effect": action ? "action" : undefined, style: { "--agent-operation-pointer-duration": `${motion.durationMs}ms`,
104
- "--agent-operation-pointer-x": `${motion.position.left}px`, "--agent-operation-pointer-y": `${motion.position.top}px` } },
105
- React.createElement("span", { className: "agent-operation__pointer-cursor" },
106
- React.createElement(Cursor, { className: "agent-operation__pointer", size: 24, weight: "fill" })),
107
- action && feedback ? React.createElement(FeedbackLabel, { feedback: feedback }) : null) : null;
188
+ activeTargetRef.current = targetElement;
189
+ clearMovementTimer();
190
+ const revision = ++motionRevisionRef.current;
191
+ const pointerRect = pointerRef.current?.getBoundingClientRect();
192
+ const currentPosition = pointerRect
193
+ ? { left: pointerRect.left, top: pointerRect.top }
194
+ : motion.position;
195
+ const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
196
+ const durationMs = prefersReducedMotion ? 0 : pointerMoveDuration(currentPosition, targetPosition);
197
+ setMotion((current) => current
198
+ ? {
199
+ ...current,
200
+ durationMs,
201
+ phase: "moving",
202
+ position: targetPosition,
203
+ revision,
204
+ toolCallId: operation.toolCallId,
205
+ }
206
+ : current);
207
+ if (durationMs === 0)
208
+ finishMovement(revision, operation.toolCallId);
209
+ else {
210
+ // A first-frame position update may not dispatch transitionend.
211
+ movementTimerRef.current = setTimeout(() => finishMovement(revision, operation.toolCallId), durationMs + 50);
212
+ }
213
+ }, [
214
+ active,
215
+ initialPosition?.left,
216
+ initialPosition?.top,
217
+ motion,
218
+ operation.toolCallId,
219
+ targetElement,
220
+ targetPosition?.left,
221
+ targetPosition?.top,
222
+ visible,
223
+ ]);
224
+ if (!motion)
225
+ return null;
226
+ const effect = operationEffect(operation);
227
+ const activeEffect = active ? effect : undefined;
228
+ const browserTool = CENTER_BROWSER_TOOL_NAMES.has(operation.name);
229
+ return (React.createElement("div", { "aria-hidden": true, className: "agent-operation__pointer-effect", "data-active": active ? "true" : "false", "data-operation-effect": activeEffect, "data-phase": motion.phase, onTransitionEnd: (event) => {
230
+ if (event.currentTarget !== event.target
231
+ || event.propertyName !== "transform"
232
+ || motion.phase !== "moving"
233
+ || !pointerReachedTarget(event.currentTarget, motion.position))
234
+ return;
235
+ finishMovement(motion.revision, motion.toolCallId);
236
+ }, ref: pointerRef, style: {
237
+ "--agent-operation-pointer-duration": `${motion.durationMs}ms`,
238
+ "--agent-operation-pointer-x": `${motion.position.left}px`,
239
+ "--agent-operation-pointer-y": `${motion.position.top}px`,
240
+ } },
241
+ active && visible && motion.toolCallId === operation.toolCallId && browserTool && effect === "action" && motion.phase !== "hidden" ? (React.createElement(React.Fragment, null,
242
+ React.createElement("span", { className: "agent-operation__browser-halo" }),
243
+ labelActive ? React.createElement("span", { className: "agent-operation__browser-label", title: label }, label) : null)) : null,
244
+ active && visible && motion.toolCallId === operation.toolCallId && !browserTool && effect === "action" && motion.phase !== "hidden" ? (React.createElement(React.Fragment, null,
245
+ React.createElement("span", { className: "agent-operation__pointer-cursor" },
246
+ React.createElement(Cursor, { className: "agent-operation__pointer", size: 24, weight: "fill" })))) : null,
247
+ active && (!browserTool || (effect === "read" && visible && motion.toolCallId === operation.toolCallId))
248
+ && motion.phase !== "hidden" && (labelActive || effect !== "action") ? (React.createElement("span", { className: labelActive ? "agent-operation__pointer-label" : "agent-operation__pointer-indicator", "data-browser-tool": browserTool ? "true" : undefined, "data-operation-effect": activeEffect, title: labelActive ? label : undefined },
249
+ activeEffect === "processing" ? React.createElement("span", { className: "agent-operation__pointer-processing" }) : null,
250
+ activeEffect === "read" ? (React.createElement("span", { className: "agent-operation__pointer-read-scan" },
251
+ React.createElement("i", null),
252
+ React.createElement("i", null),
253
+ React.createElement("i", null),
254
+ React.createElement("i", null),
255
+ React.createElement("i", null))) : null,
256
+ activeEffect === "navigate" ? React.createElement("span", { className: "agent-operation__pointer-navigation-trail" }) : null,
257
+ labelActive ? React.createElement("span", { className: "agent-operation__pointer-label-text" }, label) : null)) : null));
108
258
  }
109
- export function OperationOverlayView({ operation, feedback, progressHistory = [], running, humanHasControl = false, cancelling = false, onCancel: cancel, resolveSurface }) {
259
+ export function OperationOverlayView({ operation, progressHistory = [], running, feedbackActive = running, labelActive = feedbackActive, humanHasControl = false, cancelling = false, onCancel, resolveSurface, resolveTarget, fallbackPosition, onPointerReady }) {
110
260
  const [, setGeometry] = useState("");
261
+ useLayoutEffect(() => {
262
+ const rect = resolveSurface?.()?.getBoundingClientRect();
263
+ if (rect)
264
+ setGeometry([rect.left, rect.top, rect.width, rect.height].join(","));
265
+ }, [resolveSurface]);
111
266
  const [historyOpen, setHistoryOpen] = useState(false);
112
267
  const [historyId] = useState(() => `nexface-operation-history-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}`);
268
+ const clearTimerRef = useRef(undefined);
113
269
  const cancelButtonRef = useRef(null);
114
- const controllerShouldBeVisible = operation.status !== "cancelled" && (running || humanHasControl || operation.status === "failed");
115
- const [controllerVisibility, setControllerVisibility] = useState(controllerShouldBeVisible ? "visible" : undefined);
270
+ const controllerTimerRef = useRef(undefined);
271
+ const [visibleOperation, setVisibleOperation] = useState(operation);
272
+ const [controllerMounted, setControllerMounted] = useState(Boolean(operation
273
+ && operation.status !== "cancelled"
274
+ && (running || humanHasControl || operation.status === "failed")));
275
+ const [controllerExiting, setControllerExiting] = useState(false);
276
+ useLayoutEffect(() => {
277
+ if (clearTimerRef.current)
278
+ clearTimeout(clearTimerRef.current);
279
+ if (operation)
280
+ setVisibleOperation(operation);
281
+ if (operation?.status === "cancelled") {
282
+ clearTimerRef.current = setTimeout(() => setVisibleOperation(undefined), 600);
283
+ }
284
+ return () => { if (clearTimerRef.current)
285
+ clearTimeout(clearTimerRef.current); };
286
+ }, [operation, running]);
287
+ useEffect(() => {
288
+ if ((!running && !feedbackActive && !humanHasControl) || !visibleOperation || visibleOperation.status === "cancelled")
289
+ return;
290
+ let frame = requestAnimationFrame(function update() {
291
+ const surface = resolveSurface?.();
292
+ const target = resolveTarget?.(visibleOperation)?.element;
293
+ const geometry = [surface, target].map((element) => {
294
+ if (!element?.isConnected)
295
+ return "";
296
+ const rect = element.getBoundingClientRect();
297
+ return [rect.left, rect.top, rect.width, rect.height].join(",");
298
+ }).join(";");
299
+ setGeometry(geometry);
300
+ frame = requestAnimationFrame(update);
301
+ });
302
+ return () => cancelAnimationFrame(frame);
303
+ }, [visibleOperation?.toolCallId, visibleOperation?.status, running, feedbackActive, humanHasControl, resolveSurface, resolveTarget]);
304
+ useEffect(() => {
305
+ if (progressHistory.length === 0)
306
+ setHistoryOpen(false);
307
+ }, [progressHistory.length]);
308
+ const controllerShouldBeVisible = Boolean(visibleOperation
309
+ && visibleOperation.status !== "cancelled"
310
+ && (running || humanHasControl || visibleOperation.status === "failed"));
116
311
  useEffect(() => {
312
+ if (controllerTimerRef.current)
313
+ clearTimeout(controllerTimerRef.current);
117
314
  if (controllerShouldBeVisible) {
118
- setControllerVisibility("visible");
315
+ setControllerMounted(true);
316
+ setControllerExiting(false);
317
+ return;
318
+ }
319
+ if (!controllerMounted) {
320
+ setControllerExiting(false);
119
321
  return;
120
322
  }
121
323
  setHistoryOpen(false);
122
- setControllerVisibility((current) => current ? "exiting" : undefined);
123
- const timer = setTimeout(() => setControllerVisibility(undefined), 300);
124
- return () => clearTimeout(timer);
125
- }, [controllerShouldBeVisible]);
126
- useEffect(() => { if (!progressHistory.length)
127
- setHistoryOpen(false); }, [progressHistory.length]);
324
+ setControllerExiting(true);
325
+ controllerTimerRef.current = setTimeout(() => {
326
+ setControllerMounted(false);
327
+ setControllerExiting(false);
328
+ }, CONTROLLER_EXIT_DURATION_MS);
329
+ return () => {
330
+ if (controllerTimerRef.current)
331
+ clearTimeout(controllerTimerRef.current);
332
+ };
333
+ }, [controllerMounted, controllerShouldBeVisible]);
128
334
  useLayoutEffect(() => {
129
335
  const surface = resolveSurface?.();
130
336
  if (!surface || !running || humanHasControl)
131
337
  return;
132
338
  return lockOperationSurface(surface, () => cancelButtonRef.current?.focus());
133
339
  }, [humanHasControl, resolveSurface, running]);
134
- const readGeometry = useCallback(() => {
135
- const surface = resolveSurface?.();
136
- const resolvedTarget = feedback?.resolveTarget ? feedback.resolveTarget() : feedback?.target;
137
- const target = resolvedTarget?.isConnected && surface?.contains(resolvedTarget) ? resolvedTarget : undefined;
138
- return {
139
- surfaceRect: surface?.isConnected ? surface.getBoundingClientRect() : undefined,
140
- target,
141
- targetRect: target?.getBoundingClientRect(),
142
- };
143
- }, [resolveSurface, feedback?.target, feedback?.resolveTarget]);
144
- useLayoutEffect(() => {
145
- const update = () => {
146
- const { surfaceRect, targetRect } = readGeometry();
147
- const geometry = [surfaceRect, targetRect].map((rect) => {
148
- if (!rect)
149
- return "";
150
- return [rect.left, rect.top, rect.width, rect.height].join(",");
151
- }).join(";");
152
- setGeometry(geometry);
153
- };
154
- update();
155
- if (!running && !humanHasControl)
156
- return;
157
- let frame = requestAnimationFrame(function track() { update(); frame = requestAnimationFrame(track); });
158
- return () => cancelAnimationFrame(frame);
159
- }, [readGeometry, operation, running, humanHasControl]);
160
- if (typeof document === "undefined")
340
+ if (!visibleOperation || typeof document === "undefined")
161
341
  return null;
162
- const { surfaceRect, target, targetRect: rect } = readGeometry();
163
- const structuredTargetLost = Boolean(feedback?.requiresTarget && !target);
164
- const activeFeedback = !cancelling && !humanHasControl && !structuredTargetLost ? feedback : undefined;
165
- const effect = activeFeedback?.effect;
166
- const highlightedRect = effect === "navigate" || activeFeedback?.centered ? undefined : rect;
167
- const agentControlsSurface = running && !humanHasControl;
168
- const operationFeedbackActive = Boolean(activeFeedback);
169
- const controllerMounted = Boolean(controllerVisibility);
170
- const controllerExiting = controllerVisibility === "exiting";
171
- const controllerLabel = humanHasControl ? undefined : operation.progressLabel;
172
- const compactStatusLabel = humanHasControl ? "等待你完成操作" : operation.progressLabel;
342
+ const resolvedTarget = resolveTarget?.(visibleOperation);
343
+ const surfaceElement = resolveSurface?.();
344
+ const target = resolvedTarget && (!surfaceElement || surfaceElement.contains(resolvedTarget.element))
345
+ ? resolvedTarget
346
+ : undefined;
347
+ const structuredTargetLost = visibleOperation.name === "nxf_applyAction" && !target;
348
+ const progressLabel = operationProgressLabel(visibleOperation);
349
+ const effect = operationEffect(visibleOperation);
350
+ const targetLabel = operationTargetLabel(visibleOperation, target);
351
+ const pointerLabel = normalizedLabel(visibleOperation.progressLabel) ?? targetLabel ?? progressLabel;
352
+ const controllerLabel = humanHasControl ? undefined : progressLabel;
173
353
  const statusLabel = humanHasControl ? "等待你完成操作" : {
174
- running: "Agent 正在控制", completed: running ? "工具已完成,任务继续中" : "操作已完成",
175
- failed: running ? "Agent 正在处理错误" : "操作失败", cancelled: "任务已终止",
176
- }[operation.status];
177
- const targetPosition = rect ? center(rect) : undefined;
178
- return createPortal(React.createElement("div", { className: "agent-operation", "data-nexface-ignore": true, "data-feedback-active": operationFeedbackActive, "data-structured-target-lost": structuredTargetLost, "data-human-control": humanHasControl, "data-operation-effect": effect ?? "action", "data-run-active": running, "data-status": operation.status },
354
+ running: "Agent 正在控制",
355
+ completed: running ? "工具已完成,任务继续中" : "操作已完成",
356
+ failed: running ? "Agent 正在处理错误" : "操作失败",
357
+ cancelled: "任务已终止",
358
+ }[visibleOperation.status];
359
+ const compactStatusLabel = humanHasControl
360
+ ? "等待你完成操作"
361
+ : progressLabel;
362
+ const rect = target?.element.isConnected ? target.element.getBoundingClientRect() : undefined;
363
+ const highlightedRect = effect === "navigate" ? undefined : rect;
364
+ const surfaceRect = surfaceElement?.isConnected ? surfaceElement.getBoundingClientRect() : undefined;
365
+ const agentControlsSurface = running && !humanHasControl;
366
+ const operationFeedbackActive = feedbackActive && !structuredTargetLost && !humanHasControl && visibleOperation.status !== "cancelled";
367
+ const pointerVisible = Boolean(surfaceRect) && operationFeedbackActive;
368
+ const pointerPosition = rect ? {
369
+ left: rect.left + rect.width / 2,
370
+ top: rect.top + rect.height / 2,
371
+ } : undefined;
372
+ const cancel = onCancel ? () => {
373
+ onCancel();
374
+ } : undefined;
375
+ return createPortal(React.createElement("div", { className: "agent-operation", "data-nexface-ignore": true, "data-feedback-active": operationFeedbackActive, "data-structured-target-lost": structuredTargetLost, "data-human-control": humanHasControl, "data-operation-effect": effect, "data-run-active": running, "data-status": visibleOperation.status },
179
376
  agentControlsSurface && surfaceRect ? React.createElement("div", { "aria-hidden": true, className: "agent-operation__surface", style: {
180
- ...(highlightedRect ? { "--agent-operation-cutout-height": `${highlightedRect.height}px`,
377
+ ...(highlightedRect ? {
378
+ "--agent-operation-cutout-height": `${highlightedRect.height}px`,
181
379
  "--agent-operation-cutout-left": `${highlightedRect.left - surfaceRect.left}px`,
182
- "--agent-operation-cutout-top": `${highlightedRect.top - surfaceRect.top}px`, "--agent-operation-cutout-width": `${highlightedRect.width}px` } : {}),
380
+ "--agent-operation-cutout-top": `${highlightedRect.top - surfaceRect.top}px`,
381
+ "--agent-operation-cutout-width": `${highlightedRect.width}px`,
382
+ } : {}),
183
383
  height: surfaceRect.height, left: surfaceRect.left, top: surfaceRect.top, width: surfaceRect.width,
184
384
  } }) : null,
185
- activeFeedback && highlightedRect ? React.createElement("div", { "aria-hidden": true, className: "agent-operation__target", "data-operation-effect": effect, style: { height: highlightedRect.height, left: highlightedRect.left, top: highlightedRect.top, width: highlightedRect.width } }) : null,
186
- React.createElement(AgentPointer, { active: agentControlsSurface && !cancelling, feedback: feedback?.presentation ? feedback : activeFeedback, targetPosition: targetPosition, surfaceCenter: surfaceRect ? center(surfaceRect) : undefined }),
187
- activeFeedback && effect !== "action" && surfaceRect ? React.createElement(FeedbackIndicator, { key: activeFeedback.id, feedback: activeFeedback, position: activeFeedback.centered ? center(surfaceRect) : targetPosition }) : null,
385
+ operationFeedbackActive && highlightedRect ? React.createElement("div", { "aria-hidden": true, className: "agent-operation__target", "data-operation-effect": effect, style: { height: highlightedRect.height, left: highlightedRect.left, top: highlightedRect.top, width: highlightedRect.width } }) : null,
386
+ React.createElement(AgentPointer, { active: operationFeedbackActive, initialPosition: visibleOperation.name === "nxf_applyAction" && surfaceRect
387
+ ? { left: surfaceRect.left + surfaceRect.width / 2, top: surfaceRect.top + surfaceRect.height / 2 }
388
+ : pointerPosition ?? fallbackPosition ?? (surfaceRect ? { left: surfaceRect.left + surfaceRect.width / 2, top: surfaceRect.top + surfaceRect.height / 2 } : undefined), label: pointerLabel, labelActive: labelActive, operation: visibleOperation, targetElement: target?.element, targetPosition: pointerPosition, visible: pointerVisible, ...(onPointerReady ? { onPointerReady } : {}) }),
188
389
  controllerMounted ? React.createElement("section", { "aria-live": "polite", className: "agent-operation__controller", "data-visibility": controllerExiting ? "exiting" : "visible", style: surfaceRect ? { left: surfaceRect.left + surfaceRect.width / 2, right: "auto", "--agent-operation-controller-x": "-50%", maxWidth: Math.max(0, surfaceRect.width - 32) } : undefined },
189
390
  historyOpen ? React.createElement("aside", { "aria-label": "\u672C\u6B21\u8FD0\u884C\u7684\u64CD\u4F5C\u8BB0\u5F55", className: "agent-operation__history", id: historyId },
190
391
  React.createElement("header", null,
@@ -206,7 +407,7 @@ export function OperationOverlayView({ operation, feedback, progressHistory = []
206
407
  React.createElement("strong", null, controllerLabel)),
207
408
  statusLabel ? React.createElement("small", { className: "agent-operation__description" },
208
409
  React.createElement("span", { className: "agent-operation__status" }, statusLabel)) : null),
209
- (running || humanHasControl) && operation.status !== "cancelled" ? React.createElement("span", { className: "agent-operation__compact-status", title: compactStatusLabel }, compactStatusLabel) : null,
210
- cancel && (running || humanHasControl) && operation.status !== "cancelled" ? React.createElement("button", { ref: cancelButtonRef, className: "agent-operation__stop", type: "button", disabled: cancelling, onClick: cancel }, cancelling ? "正在终止" : "终止任务") : null) : null), document.body);
410
+ (running || humanHasControl) && visibleOperation.status !== "cancelled" ? React.createElement("span", { className: "agent-operation__compact-status", title: compactStatusLabel }, compactStatusLabel) : null,
411
+ cancel && (running || humanHasControl) && visibleOperation.status !== "cancelled" ? React.createElement("button", { ref: cancelButtonRef, className: "agent-operation__stop", type: "button", disabled: cancelling, onClick: cancel }, cancelling ? "正在终止" : "终止任务") : null) : null), document.body);
211
412
  }
212
413
  //# sourceMappingURL=operation-view.js.map