@blario/react-native 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +377 -2
- package/dist/BlarProvider.d.ts +74 -0
- package/dist/actions/batched.d.ts +1 -0
- package/dist/actions/click.d.ts +4 -0
- package/dist/actions/commit-watch.d.ts +15 -0
- package/dist/actions/events.d.ts +35 -0
- package/dist/actions/executor.d.ts +29 -0
- package/dist/actions/highlight.d.ts +4 -0
- package/dist/actions/interaction-guards.d.ts +9 -0
- package/dist/actions/landing.d.ts +11 -0
- package/dist/actions/navigation-driver.d.ts +22 -0
- package/dist/actions/parse.d.ts +4 -0
- package/dist/actions/result.d.ts +4 -0
- package/dist/actions/scrolling.d.ts +12 -0
- package/dist/actions/services.d.ts +20 -0
- package/dist/actions/settle.d.ts +13 -0
- package/dist/actions/switch.d.ts +3 -0
- package/dist/actions/target.d.ts +25 -0
- package/dist/actions/text-input.d.ts +9 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/agent/batch-commands.d.ts +17 -0
- package/dist/agent/compose.d.ts +23 -0
- package/dist/agent/controller.d.ts +28 -0
- package/dist/agent/frame-runner.d.ts +9 -0
- package/dist/agent/frames.d.ts +180 -0
- package/dist/agent/handlers/agent-command.d.ts +5 -0
- package/dist/agent/handlers/batch.d.ts +5 -0
- package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
- package/dist/agent/handlers/context.d.ts +16 -0
- package/dist/agent/handlers/dom-state.d.ts +4 -0
- package/dist/agent/handlers/element-context.d.ts +2 -0
- package/dist/agent/handlers/screenshot.d.ts +3 -0
- package/dist/agent/handlers/text-dom.d.ts +5 -0
- package/dist/agent/stage.d.ts +13 -0
- package/dist/agent-mode/action-description.d.ts +3 -0
- package/dist/agent-mode/bubbles.d.ts +10 -0
- package/dist/agent-mode/colors.d.ts +12 -0
- package/dist/agent-mode/controller.d.ts +92 -0
- package/dist/agent-mode/cursor-follow.d.ts +12 -0
- package/dist/agent-mode/cursor.d.ts +37 -0
- package/dist/agent-mode/frames.d.ts +36 -0
- package/dist/agent-mode/haptics.d.ts +6 -0
- package/dist/agent-mode/keep-awake.d.ts +19 -0
- package/dist/agent-mode/layer-stacking.d.ts +7 -0
- package/dist/agent-mode/markdown.d.ts +19 -0
- package/dist/agent-mode/panel-geometry.d.ts +6 -0
- package/dist/agent-mode/resume.d.ts +13 -0
- package/dist/agent-mode/run-memory.d.ts +19 -0
- package/dist/agent-mode/spotlight-controller.d.ts +68 -0
- package/dist/agent-mode/spotlight.d.ts +6 -0
- package/dist/agent-mode/state.d.ts +9 -0
- package/dist/agent-mode/status.d.ts +11 -0
- package/dist/agent-mode/timers.d.ts +6 -0
- package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
- package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
- package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
- package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
- package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
- package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
- package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
- package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
- package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
- package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
- package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
- package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
- package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
- package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
- package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
- package/dist/agent-mode/ui/layer-host.d.ts +2 -0
- package/dist/backend/api-client.d.ts +38 -0
- package/dist/backend/api-types.d.ts +25 -0
- package/dist/backend/app-info.d.ts +14 -0
- package/dist/backend/backend-transport.d.ts +70 -0
- package/dist/backend/backoff.d.ts +9 -0
- package/dist/backend/base64.d.ts +1 -0
- package/dist/backend/errors.d.ts +17 -0
- package/dist/backend/foreground.d.ts +1 -0
- package/dist/backend/health.d.ts +17 -0
- package/dist/backend/identity-proof.d.ts +23 -0
- package/dist/backend/identity.d.ts +27 -0
- package/dist/backend/message-metadata.d.ts +19 -0
- package/dist/backend/presence.d.ts +20 -0
- package/dist/backend/response-cache.d.ts +9 -0
- package/dist/backend/session-change.d.ts +2 -0
- package/dist/backend/session-socket.d.ts +49 -0
- package/dist/backend/session.d.ts +39 -0
- package/dist/backend/socket-url.d.ts +9 -0
- package/dist/backend/storage.d.ts +13 -0
- package/dist/backend/tab-id.d.ts +2 -0
- package/dist/blar.d.ts +37 -0
- package/dist/chat/ChatLayer.d.ts +29 -0
- package/dist/chat/ChatSheet.d.ts +11 -0
- package/dist/chat/ChatView.d.ts +19 -0
- package/dist/chat/Launcher.d.ts +14 -0
- package/dist/chat/bridge.d.ts +22 -0
- package/dist/chat/chat-coordinator.d.ts +57 -0
- package/dist/chat/chat-state.d.ts +20 -0
- package/dist/chat/navigation.d.ts +5 -0
- package/dist/chat/options.d.ts +22 -0
- package/dist/chat/webview.d.ts +10 -0
- package/dist/chat/widget-bundle.generated.d.ts +2 -0
- package/dist/chat/widget-shell.d.ts +24 -0
- package/dist/confirmation/port.d.ts +12 -0
- package/dist/fiber/locate.d.ts +7 -0
- package/dist/fiber/narrow.d.ts +5 -0
- package/dist/fiber/reader.d.ts +10 -0
- package/dist/fiber/text.d.ts +4 -0
- package/dist/fiber/types.d.ts +21 -0
- package/dist/host/border-radius.d.ts +1 -0
- package/dist/host/instance.d.ts +9 -0
- package/dist/host/measure.d.ts +3 -0
- package/dist/host/rect.d.ts +22 -0
- package/dist/host/visible-area.d.ts +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/nudge/NudgeBubble.d.ts +15 -0
- package/dist/nudge/nudge.d.ts +23 -0
- package/dist/optional/expo-haptics.d.ts +1 -0
- package/dist/optional/expo-keep-awake.d.ts +1 -0
- package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
- package/dist/optional/react-native-keep-awake.d.ts +1 -0
- package/dist/overlay/AgentOverlay.d.ts +5 -0
- package/dist/screenshot/capture-backend.d.ts +6 -0
- package/dist/screenshot/screenshotter.d.ts +18 -0
- package/dist/screenshot/skia.d.ts +35 -0
- package/dist/screenshot/view-shot.d.ts +2 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/classify.d.ts +11 -0
- package/dist/snapshot/collect.d.ts +27 -0
- package/dist/snapshot/element-context.d.ts +9 -0
- package/dist/snapshot/icon.d.ts +2 -0
- package/dist/snapshot/kinds.d.ts +3 -0
- package/dist/snapshot/modals.d.ts +2 -0
- package/dist/snapshot/navigation.d.ts +14 -0
- package/dist/snapshot/ordering.d.ts +2 -0
- package/dist/snapshot/properties.d.ts +14 -0
- package/dist/snapshot/registry.d.ts +16 -0
- package/dist/snapshot/render.d.ts +10 -0
- package/dist/snapshot/route-summary.d.ts +3 -0
- package/dist/snapshot/scroll.d.ts +15 -0
- package/dist/snapshot/text-layout.d.ts +8 -0
- package/dist/snapshot/view.d.ts +30 -0
- package/dist/snapshot/visibility.d.ts +17 -0
- package/dist/snapshot/wrappers.d.ts +11 -0
- package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
- package/dist/telemetry/build-info.generated.d.ts +2 -0
- package/dist/telemetry/envelope.d.ts +53 -0
- package/dist/telemetry/reporter.d.ts +33 -0
- package/dist/telemetry/scrub.d.ts +1 -0
- package/dist/telemetry/sdk-reporter.d.ts +5 -0
- package/dist/transport/connection-options.d.ts +18 -0
- package/dist/transport/relay-transport.d.ts +17 -0
- package/dist/transport/transport.d.ts +7 -0
- package/dist/ui/insets.d.ts +8 -0
- package/dist/ui/strings.d.ts +33 -0
- package/dist/ui/useInsets.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/package.json +85 -4
- package/src/BlarProvider.tsx +462 -0
- package/src/actions/batched.ts +16 -0
- package/src/actions/click.ts +54 -0
- package/src/actions/commit-watch.ts +86 -0
- package/src/actions/events.ts +58 -0
- package/src/actions/executor.ts +147 -0
- package/src/actions/highlight.ts +13 -0
- package/src/actions/interaction-guards.ts +44 -0
- package/src/actions/landing.ts +26 -0
- package/src/actions/navigation-driver.ts +131 -0
- package/src/actions/parse.ts +82 -0
- package/src/actions/result.ts +14 -0
- package/src/actions/scrolling.ts +112 -0
- package/src/actions/services.ts +24 -0
- package/src/actions/settle.ts +40 -0
- package/src/actions/switch.ts +31 -0
- package/src/actions/target.ts +77 -0
- package/src/actions/text-input.ts +82 -0
- package/src/actions/types.ts +52 -0
- package/src/agent/batch-commands.ts +33 -0
- package/src/agent/compose.ts +67 -0
- package/src/agent/controller.ts +106 -0
- package/src/agent/frame-runner.ts +27 -0
- package/src/agent/frames.ts +193 -0
- package/src/agent/handlers/agent-command.ts +74 -0
- package/src/agent/handlers/batch.ts +72 -0
- package/src/agent/handlers/consent-screenshot.ts +44 -0
- package/src/agent/handlers/context.ts +17 -0
- package/src/agent/handlers/dom-state.ts +19 -0
- package/src/agent/handlers/element-context.ts +9 -0
- package/src/agent/handlers/screenshot.ts +24 -0
- package/src/agent/handlers/text-dom.ts +17 -0
- package/src/agent/stage.ts +22 -0
- package/src/agent-mode/action-description.ts +25 -0
- package/src/agent-mode/bubbles.ts +31 -0
- package/src/agent-mode/colors.ts +40 -0
- package/src/agent-mode/controller.ts +459 -0
- package/src/agent-mode/cursor-follow.ts +43 -0
- package/src/agent-mode/cursor.ts +118 -0
- package/src/agent-mode/frames.ts +70 -0
- package/src/agent-mode/haptics.ts +49 -0
- package/src/agent-mode/keep-awake.ts +107 -0
- package/src/agent-mode/layer-stacking.ts +16 -0
- package/src/agent-mode/markdown.ts +84 -0
- package/src/agent-mode/panel-geometry.ts +27 -0
- package/src/agent-mode/resume.ts +16 -0
- package/src/agent-mode/run-memory.ts +65 -0
- package/src/agent-mode/spotlight-controller.ts +255 -0
- package/src/agent-mode/spotlight.ts +14 -0
- package/src/agent-mode/state.ts +36 -0
- package/src/agent-mode/status.ts +13 -0
- package/src/agent-mode/timers.ts +11 -0
- package/src/agent-mode/ui/AgentDock.tsx +285 -0
- package/src/agent-mode/ui/AgentLayer.tsx +123 -0
- package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
- package/src/agent-mode/ui/AgentPanel.tsx +187 -0
- package/src/agent-mode/ui/AgentTap.tsx +190 -0
- package/src/agent-mode/ui/MarkdownText.tsx +55 -0
- package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
- package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
- package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
- package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
- package/src/agent-mode/ui/StatusCard.tsx +137 -0
- package/src/agent-mode/ui/TargetPulse.tsx +74 -0
- package/src/agent-mode/ui/TouchShield.tsx +22 -0
- package/src/agent-mode/ui/WarningIcon.tsx +32 -0
- package/src/agent-mode/ui/full-window-overlay.ts +16 -0
- package/src/agent-mode/ui/layer-host.ts +11 -0
- package/src/backend/api-client.ts +210 -0
- package/src/backend/api-types.ts +56 -0
- package/src/backend/app-info.ts +40 -0
- package/src/backend/backend-transport.ts +338 -0
- package/src/backend/backoff.ts +16 -0
- package/src/backend/base64.ts +25 -0
- package/src/backend/errors.ts +32 -0
- package/src/backend/foreground.ts +10 -0
- package/src/backend/health.ts +64 -0
- package/src/backend/identity-proof.ts +151 -0
- package/src/backend/identity.ts +31 -0
- package/src/backend/message-metadata.ts +28 -0
- package/src/backend/presence.ts +95 -0
- package/src/backend/response-cache.ts +45 -0
- package/src/backend/session-change.ts +14 -0
- package/src/backend/session-socket.ts +292 -0
- package/src/backend/session.ts +188 -0
- package/src/backend/socket-url.ts +25 -0
- package/src/backend/storage.ts +49 -0
- package/src/backend/tab-id.ts +24 -0
- package/src/blar.ts +100 -0
- package/src/chat/ChatLayer.tsx +109 -0
- package/src/chat/ChatSheet.tsx +82 -0
- package/src/chat/ChatView.tsx +127 -0
- package/src/chat/Launcher.tsx +72 -0
- package/src/chat/bridge.ts +56 -0
- package/src/chat/chat-coordinator.ts +189 -0
- package/src/chat/chat-state.ts +35 -0
- package/src/chat/navigation.ts +15 -0
- package/src/chat/options.ts +39 -0
- package/src/chat/webview.ts +23 -0
- package/src/chat/widget-bundle.generated.ts +2 -0
- package/src/chat/widget-shell.ts +170 -0
- package/src/confirmation/port.ts +15 -0
- package/src/fiber/locate.ts +41 -0
- package/src/fiber/narrow.ts +15 -0
- package/src/fiber/reader.ts +98 -0
- package/src/fiber/text.ts +52 -0
- package/src/fiber/types.ts +24 -0
- package/src/host/border-radius.ts +11 -0
- package/src/host/instance.ts +30 -0
- package/src/host/measure.ts +25 -0
- package/src/host/rect.ts +45 -0
- package/src/host/visible-area.ts +5 -0
- package/src/index.ts +10 -0
- package/src/nudge/NudgeBubble.tsx +117 -0
- package/src/nudge/nudge.ts +100 -0
- package/src/optional/expo-haptics.ts +11 -0
- package/src/optional/expo-keep-awake.ts +11 -0
- package/src/optional/react-native-haptic-feedback.ts +11 -0
- package/src/optional/react-native-keep-awake.ts +11 -0
- package/src/overlay/AgentOverlay.tsx +16 -0
- package/src/screenshot/capture-backend.ts +7 -0
- package/src/screenshot/screenshotter.ts +39 -0
- package/src/screenshot/skia.ts +107 -0
- package/src/screenshot/view-shot.ts +41 -0
- package/src/snapshot/capture.ts +163 -0
- package/src/snapshot/classify.ts +40 -0
- package/src/snapshot/collect.ts +136 -0
- package/src/snapshot/element-context.ts +66 -0
- package/src/snapshot/icon.ts +21 -0
- package/src/snapshot/kinds.ts +10 -0
- package/src/snapshot/modals.ts +21 -0
- package/src/snapshot/navigation.ts +47 -0
- package/src/snapshot/ordering.ts +26 -0
- package/src/snapshot/properties.ts +71 -0
- package/src/snapshot/registry.ts +33 -0
- package/src/snapshot/render.ts +140 -0
- package/src/snapshot/route-summary.ts +15 -0
- package/src/snapshot/scroll.ts +90 -0
- package/src/snapshot/text-layout.ts +59 -0
- package/src/snapshot/view.ts +87 -0
- package/src/snapshot/visibility.ts +63 -0
- package/src/snapshot/wrappers.ts +69 -0
- package/src/telemetry/SdkErrorBoundary.tsx +28 -0
- package/src/telemetry/build-info.generated.ts +2 -0
- package/src/telemetry/envelope.ts +83 -0
- package/src/telemetry/reporter.ts +170 -0
- package/src/telemetry/scrub.ts +10 -0
- package/src/telemetry/sdk-reporter.ts +37 -0
- package/src/transport/connection-options.ts +65 -0
- package/src/transport/relay-transport.ts +82 -0
- package/src/transport/transport.ts +8 -0
- package/src/ui/insets.ts +31 -0
- package/src/ui/strings.ts +102 -0
- package/src/ui/useInsets.ts +10 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { fiberRootOf } from '../fiber/reader.ts';
|
|
2
|
+
import type { Fiber, FiberRoot } from '../fiber/types.ts';
|
|
3
|
+
|
|
4
|
+
export class CommitWatch {
|
|
5
|
+
private watchedRoot: FiberRoot | null = null;
|
|
6
|
+
|
|
7
|
+
private originalDescriptor: PropertyDescriptor | null = null;
|
|
8
|
+
|
|
9
|
+
private watchedSetter: ((next: Fiber) => void) | null = null;
|
|
10
|
+
|
|
11
|
+
private commits = 0;
|
|
12
|
+
|
|
13
|
+
private baseline = 0;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly getAnchorFiber: () => Fiber | null,
|
|
17
|
+
private readonly onCommit?: () => void,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
begin(): void {
|
|
21
|
+
this.install();
|
|
22
|
+
this.baseline = this.commits;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
hasCommitted(): boolean {
|
|
26
|
+
return this.commits > this.baseline;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
release(): void {
|
|
30
|
+
const root = this.watchedRoot;
|
|
31
|
+
if (root === null) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const descriptor = this.originalDescriptor;
|
|
35
|
+
const setter = this.watchedSetter;
|
|
36
|
+
this.watchedRoot = null;
|
|
37
|
+
this.originalDescriptor = null;
|
|
38
|
+
this.watchedSetter = null;
|
|
39
|
+
try {
|
|
40
|
+
if (descriptor !== null && Object.getOwnPropertyDescriptor(root, 'current')?.set === setter) {
|
|
41
|
+
Object.defineProperty(root, 'current', { ...descriptor, value: root.current });
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private install(): void {
|
|
49
|
+
const anchor = this.getAnchorFiber();
|
|
50
|
+
const root = anchor === null ? null : fiberRootOf(anchor);
|
|
51
|
+
if (root === null || root === this.watchedRoot) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.release();
|
|
55
|
+
const descriptor = Object.getOwnPropertyDescriptor(root, 'current');
|
|
56
|
+
if (descriptor === undefined || !descriptor.configurable || !descriptor.writable || !('value' in descriptor)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let latest = root.current;
|
|
60
|
+
const setter = (next: Fiber): void => {
|
|
61
|
+
const committed = next !== latest;
|
|
62
|
+
latest = next;
|
|
63
|
+
if (committed && this.watchedRoot === root) {
|
|
64
|
+
this.commits += 1;
|
|
65
|
+
try {
|
|
66
|
+
this.onCommit?.();
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
Object.defineProperty(root, 'current', {
|
|
74
|
+
configurable: true,
|
|
75
|
+
enumerable: descriptor.enumerable,
|
|
76
|
+
get: () => latest,
|
|
77
|
+
set: setter,
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.watchedRoot = root;
|
|
83
|
+
this.originalDescriptor = descriptor;
|
|
84
|
+
this.watchedSetter = setter;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Rect, centerOf } from '../host/rect.ts';
|
|
2
|
+
|
|
3
|
+
export interface SyntheticPressEvent {
|
|
4
|
+
nativeEvent: {
|
|
5
|
+
pageX: number;
|
|
6
|
+
pageY: number;
|
|
7
|
+
locationX: number;
|
|
8
|
+
locationY: number;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
target: number;
|
|
11
|
+
touches: never[];
|
|
12
|
+
changedTouches: never[];
|
|
13
|
+
};
|
|
14
|
+
persist: () => void;
|
|
15
|
+
preventDefault: () => void;
|
|
16
|
+
stopPropagation: () => void;
|
|
17
|
+
isPropagationStopped: () => boolean;
|
|
18
|
+
isDefaultPrevented: () => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SyntheticTextChangeEvent {
|
|
22
|
+
nativeEvent: { text: string; eventCount: number; target: number };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SyntheticSubmitEvent {
|
|
26
|
+
nativeEvent: { text: string; eventCount: number; target: number };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function noop(): void {}
|
|
30
|
+
|
|
31
|
+
export function buildPressEvent(rect: Rect): SyntheticPressEvent {
|
|
32
|
+
const center = centerOf(rect);
|
|
33
|
+
return {
|
|
34
|
+
nativeEvent: {
|
|
35
|
+
pageX: center.x,
|
|
36
|
+
pageY: center.y,
|
|
37
|
+
locationX: rect.width / 2,
|
|
38
|
+
locationY: rect.height / 2,
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
target: 0,
|
|
41
|
+
touches: [],
|
|
42
|
+
changedTouches: [],
|
|
43
|
+
},
|
|
44
|
+
persist: noop,
|
|
45
|
+
preventDefault: noop,
|
|
46
|
+
stopPropagation: noop,
|
|
47
|
+
isPropagationStopped: () => false,
|
|
48
|
+
isDefaultPrevented: () => false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildTextChangeEvent(text: string): SyntheticTextChangeEvent {
|
|
53
|
+
return { nativeEvent: { text, eventCount: 1, target: 0 } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildSubmitEvent(text: string): SyntheticSubmitEvent {
|
|
57
|
+
return { nativeEvent: { text, eventCount: 1, target: 0 } };
|
|
58
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { SnapshotCapturer } from '../snapshot/capture.ts';
|
|
2
|
+
import { click } from './click.ts';
|
|
3
|
+
import type { CommitWatch } from './commit-watch.ts';
|
|
4
|
+
import { highlight } from './highlight.ts';
|
|
5
|
+
import { tapsTarget, waitForLanding, waitsForLanding } from './landing.ts';
|
|
6
|
+
import { failed, failedFromException } from './result.ts';
|
|
7
|
+
import { scrollContainer, scrollIntoView } from './scrolling.ts';
|
|
8
|
+
import { SYSTEM_CLOCK } from './settle.ts';
|
|
9
|
+
import type { ActionServices } from './services.ts';
|
|
10
|
+
import { waitForSettle } from './settle.ts';
|
|
11
|
+
import { toggleSwitch } from './switch.ts';
|
|
12
|
+
import { measureTarget, type ResolvedTarget } from './target.ts';
|
|
13
|
+
import { submitInput, typeText } from './text-input.ts';
|
|
14
|
+
import type { ActionOutcome, AgentAction, AgentCommandData, FailedResult, HandlerResult } from './types.ts';
|
|
15
|
+
|
|
16
|
+
const ACTIONS_WITHOUT_TARGET: readonly AgentAction['type'][] = ['navigate', 'press_key'];
|
|
17
|
+
const ACTIONS_NEEDING_VISIBLE_TARGET: readonly AgentAction['type'][] = ['click', 'type', 'check', 'highlight'];
|
|
18
|
+
const REVEAL_SETTLE_MS = 150;
|
|
19
|
+
|
|
20
|
+
export interface ExecutionOptions {
|
|
21
|
+
isCancelled: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Preparation = { ok: true; target: ResolvedTarget | null } | { ok: false; outcome: ActionOutcome };
|
|
25
|
+
|
|
26
|
+
const CANCELLED_MESSAGE = 'Cancelled by the user before the action ran';
|
|
27
|
+
|
|
28
|
+
function outcomeOf(result: FailedResult): ActionOutcome {
|
|
29
|
+
return { success: false, no_op: false, error: result.error, message: result.message };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ActionExecutor {
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly capturer: SnapshotCapturer,
|
|
35
|
+
private readonly services: ActionServices,
|
|
36
|
+
private readonly commitWatch: CommitWatch,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
async prepare(command: AgentCommandData): Promise<Preparation> {
|
|
40
|
+
if (command.action.type === 'navigate') {
|
|
41
|
+
return { ok: true, target: null };
|
|
42
|
+
}
|
|
43
|
+
const target = this.resolveTarget(command);
|
|
44
|
+
if (target === null) {
|
|
45
|
+
return { ok: false, outcome: outcomeOf(this.missingTarget(command)) };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await this.revealTarget(command.action, target);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return { ok: false, outcome: outcomeOf(failedFromException(error)) };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, target };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async execute(command: AgentCommandData, target: ResolvedTarget | null, options: ExecutionOptions): Promise<ActionOutcome> {
|
|
56
|
+
const before = (await this.capturer.capture('peek')).text;
|
|
57
|
+
this.commitWatch.begin();
|
|
58
|
+
const result = await this.perform(command, target, options);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
return outcomeOf(result);
|
|
61
|
+
}
|
|
62
|
+
const after = await waitForSettle(async () => (await this.capturer.capture('peek')).text);
|
|
63
|
+
const expectsChange = command.action.type !== 'highlight';
|
|
64
|
+
const unchanged = before === after && !this.commitWatch.hasCommitted();
|
|
65
|
+
return { success: true, no_op: expectsChange && unchanged, message: result.message };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async perform(command: AgentCommandData, target: ResolvedTarget | null, options: ExecutionOptions): Promise<HandlerResult> {
|
|
69
|
+
try {
|
|
70
|
+
const { action } = command;
|
|
71
|
+
if (action.type === 'navigate') {
|
|
72
|
+
return options.isCancelled() ? failed('user_cancelled', CANCELLED_MESSAGE) : this.services.navigation.navigateTo(action.route, action.params);
|
|
73
|
+
}
|
|
74
|
+
if (target === null) {
|
|
75
|
+
return this.missingTarget(command);
|
|
76
|
+
}
|
|
77
|
+
await this.pointAt(action, target);
|
|
78
|
+
if (options.isCancelled()) {
|
|
79
|
+
return failed('user_cancelled', CANCELLED_MESSAGE);
|
|
80
|
+
}
|
|
81
|
+
return await this.dispatch(command, target);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return failedFromException(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async revealTarget(action: AgentAction, target: ResolvedTarget): Promise<void> {
|
|
88
|
+
if (!ACTIONS_NEEDING_VISIBLE_TARGET.includes(action.type)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const revealed = await scrollIntoView(target, { animated: false });
|
|
92
|
+
if (revealed.ok && revealed.message === undefined) {
|
|
93
|
+
await SYSTEM_CLOCK.sleep(REVEAL_SETTLE_MS);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async pointAt(action: AgentAction, target: ResolvedTarget): Promise<void> {
|
|
98
|
+
if (!tapsTarget(action)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.services.cursor.moveTo(measureTarget(target));
|
|
102
|
+
if (waitsForLanding(action)) {
|
|
103
|
+
await waitForLanding(this.services.cursor, SYSTEM_CLOCK);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private resolveTarget(command: AgentCommandData): ResolvedTarget | null {
|
|
108
|
+
if (command.agent_id !== null) {
|
|
109
|
+
return this.services.resolver.resolveAgentId(command.agent_id);
|
|
110
|
+
}
|
|
111
|
+
const remembered = this.services.inputMemory.last;
|
|
112
|
+
if (command.action.type === 'press_key' && remembered !== null) {
|
|
113
|
+
return this.services.resolver.resolveElement(remembered);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private missingTarget(command: AgentCommandData): FailedResult {
|
|
119
|
+
if (command.agent_id === null && !ACTIONS_WITHOUT_TARGET.includes(command.action.type)) {
|
|
120
|
+
return failed('element_not_found', 'target.agent_id is required for this action');
|
|
121
|
+
}
|
|
122
|
+
const reason = command.agent_id === null ? 'no input has been used yet' : `${command.agent_id} is not in the latest capture or is no longer mounted`;
|
|
123
|
+
return failed('element_not_found', reason);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private dispatch(command: AgentCommandData, target: ResolvedTarget): Promise<HandlerResult> | HandlerResult {
|
|
127
|
+
const { action } = command;
|
|
128
|
+
switch (action.type) {
|
|
129
|
+
case 'click':
|
|
130
|
+
return click(target, this.services);
|
|
131
|
+
case 'type':
|
|
132
|
+
return typeText(target, action, this.services);
|
|
133
|
+
case 'check':
|
|
134
|
+
return toggleSwitch(target, action.checked);
|
|
135
|
+
case 'press_key':
|
|
136
|
+
return action.key === 'Enter' ? submitInput(target) : failed('unsupported_action', `key ${action.key} is not supported`);
|
|
137
|
+
case 'scroll':
|
|
138
|
+
return scrollContainer(target, action);
|
|
139
|
+
case 'scroll_to':
|
|
140
|
+
return scrollIntoView(target);
|
|
141
|
+
case 'highlight':
|
|
142
|
+
return highlight(target, command.teaching_explanation, this.services);
|
|
143
|
+
case 'navigate':
|
|
144
|
+
return failed('unsupported_action', 'navigate does not take a target');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { failed, succeeded } from './result.ts';
|
|
2
|
+
import type { ActionServices } from './services.ts';
|
|
3
|
+
import { measureTarget, type ResolvedTarget, viewOf } from './target.ts';
|
|
4
|
+
import type { HandlerResult } from './types.ts';
|
|
5
|
+
|
|
6
|
+
export async function highlight(target: ResolvedTarget, text: string | null, services: ActionServices): Promise<HandlerResult> {
|
|
7
|
+
const rect = (await measureTarget(target)()) ?? target.element.rect;
|
|
8
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
9
|
+
return failed('element_not_found', 'the element has no on-screen size');
|
|
10
|
+
}
|
|
11
|
+
const completed = await services.reading.read({ ...viewOf(target), text });
|
|
12
|
+
return completed ? succeeded() : failed('user_cancelled', 'User cancelled during highlight');
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isRecord } from '../fiber/narrow.ts';
|
|
2
|
+
import { propsOf } from '../fiber/reader.ts';
|
|
3
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
4
|
+
import { accessibilityFlagsOf } from '../snapshot/properties.ts';
|
|
5
|
+
|
|
6
|
+
export type GuardVerdict = { allowed: true } | { allowed: false; reason: string };
|
|
7
|
+
|
|
8
|
+
const ALLOWED: GuardVerdict = { allowed: true };
|
|
9
|
+
|
|
10
|
+
function pointerEventsOf(fiber: Fiber): string | null {
|
|
11
|
+
const props = propsOf(fiber);
|
|
12
|
+
if (typeof props.pointerEvents === 'string') {
|
|
13
|
+
return props.pointerEvents;
|
|
14
|
+
}
|
|
15
|
+
const style = props.style;
|
|
16
|
+
return isRecord(style) && !Array.isArray(style) && typeof style.pointerEvents === 'string' ? style.pointerEvents : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function blocksPointerEvents(fiber: Fiber, isTarget: boolean): boolean {
|
|
20
|
+
const pointerEvents = pointerEventsOf(fiber);
|
|
21
|
+
if (pointerEvents === 'none') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return !isTarget && pointerEvents === 'box-only';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function checkInteractable(fiber: Fiber, ancestors: Fiber[]): GuardVerdict {
|
|
28
|
+
if (accessibilityFlagsOf(propsOf(fiber)).disabled) {
|
|
29
|
+
return { allowed: false, reason: 'the element is disabled' };
|
|
30
|
+
}
|
|
31
|
+
if (blocksPointerEvents(fiber, true)) {
|
|
32
|
+
return { allowed: false, reason: 'the element has pointerEvents none' };
|
|
33
|
+
}
|
|
34
|
+
const blocker = ancestors.find((ancestor) => blocksPointerEvents(ancestor, false));
|
|
35
|
+
return blocker === undefined ? ALLOWED : { allowed: false, reason: 'an ancestor blocks pointer events' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkEditable(fiber: Fiber, ancestors: Fiber[]): GuardVerdict {
|
|
39
|
+
const verdict = checkInteractable(fiber, ancestors);
|
|
40
|
+
if (!verdict.allowed) {
|
|
41
|
+
return verdict;
|
|
42
|
+
}
|
|
43
|
+
return propsOf(fiber).editable === false ? { allowed: false, reason: 'the input is not editable' } : ALLOWED;
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Rect } from '../host/rect.ts';
|
|
2
|
+
import type { Clock } from './settle.ts';
|
|
3
|
+
import type { AgentAction } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export interface CursorPort {
|
|
6
|
+
moveTo: (measure: () => Promise<Rect | null>) => void;
|
|
7
|
+
whenLanded: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const LANDING_MAX_MS = 1800;
|
|
11
|
+
|
|
12
|
+
const TAP_ACTIONS: readonly AgentAction['type'][] = ['click', 'check', 'type'];
|
|
13
|
+
|
|
14
|
+
const LANDING_ACTIONS: readonly AgentAction['type'][] = ['click', 'check'];
|
|
15
|
+
|
|
16
|
+
export function tapsTarget(action: AgentAction): boolean {
|
|
17
|
+
return TAP_ACTIONS.includes(action.type);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function waitsForLanding(action: AgentAction): boolean {
|
|
21
|
+
return LANDING_ACTIONS.includes(action.type);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function waitForLanding(cursor: CursorPort, clock: Clock): Promise<void> {
|
|
25
|
+
await Promise.race([cursor.whenLanded(), clock.sleep(LANDING_MAX_MS)]);
|
|
26
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { isFunction, isRecord, type UnknownRecord } from '../fiber/narrow.ts';
|
|
2
|
+
import { routeNamesOf, type ScreenRef } from '../snapshot/navigation.ts';
|
|
3
|
+
import { failed, succeeded } from './result.ts';
|
|
4
|
+
import type { HandlerResult } from './types.ts';
|
|
5
|
+
|
|
6
|
+
export interface NavigationRefLike {
|
|
7
|
+
isReady: () => boolean;
|
|
8
|
+
navigate(...args: unknown[]): void;
|
|
9
|
+
getRootState: () => unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NavigationSources {
|
|
13
|
+
readFocusedScreens: () => ScreenRef[];
|
|
14
|
+
getNavigationRef: () => NavigationRefLike | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function routeStatesIn(state: unknown): UnknownRecord[] {
|
|
18
|
+
if (!isRecord(state) || !Array.isArray(state.routes)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return state.routes.filter(isRecord);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pathToRoute(state: unknown, target: string): string[] | null {
|
|
25
|
+
for (const route of routeStatesIn(state)) {
|
|
26
|
+
if (route.name === target) {
|
|
27
|
+
return [target];
|
|
28
|
+
}
|
|
29
|
+
const nested = pathToRoute(route.state, target);
|
|
30
|
+
if (nested !== null && typeof route.name === 'string') {
|
|
31
|
+
return [route.name, ...nested];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nestedNavigateArguments(path: string[], params: UnknownRecord | null): [string, UnknownRecord | undefined] {
|
|
38
|
+
const [first, ...rest] = path;
|
|
39
|
+
if (first === undefined) {
|
|
40
|
+
return ['', undefined];
|
|
41
|
+
}
|
|
42
|
+
if (rest.length === 0) {
|
|
43
|
+
return [first, params ?? undefined];
|
|
44
|
+
}
|
|
45
|
+
const [screen, nestedParams] = nestedNavigateArguments(rest, params);
|
|
46
|
+
return [first, { screen, params: nestedParams }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class NavigationDriver {
|
|
50
|
+
constructor(private readonly sources: NavigationSources) {}
|
|
51
|
+
|
|
52
|
+
listRefRoutePaths(): string[] {
|
|
53
|
+
const ref = this.sources.getNavigationRef();
|
|
54
|
+
if (ref === null || !ref.isReady()) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const paths = new Set<string>();
|
|
58
|
+
this.collectRoutePaths(ref.getRootState(), [], paths);
|
|
59
|
+
return [...paths];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
navigateTo(route: string, params: UnknownRecord | null): HandlerResult {
|
|
63
|
+
const path = route.split('/').filter((segment) => segment.length > 0);
|
|
64
|
+
if (path.length > 1) {
|
|
65
|
+
return this.navigateAlongPath(path, params);
|
|
66
|
+
}
|
|
67
|
+
const direct = this.navigateThroughFocusedScreens(route, params);
|
|
68
|
+
if (direct.ok) {
|
|
69
|
+
return direct;
|
|
70
|
+
}
|
|
71
|
+
return this.navigateThroughRef(route, params) ?? direct;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private navigateAlongPath(path: string[], params: UnknownRecord | null): HandlerResult {
|
|
75
|
+
const [head] = path;
|
|
76
|
+
const innermostFirst = [...this.sources.readFocusedScreens()].reverse();
|
|
77
|
+
const owner = innermostFirst.find((screen) => head !== undefined && routeNamesOf(screen.navigation).includes(head));
|
|
78
|
+
if (owner === undefined) {
|
|
79
|
+
return failed('element_not_found', `route "${head}" is not reachable from the focused screens`);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const [name, nested] = nestedNavigateArguments(path, params);
|
|
83
|
+
owner.navigation.navigate(...(nested === undefined ? [name] : [name, nested]));
|
|
84
|
+
return succeeded();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return failed('execution_error', error instanceof Error ? error.message : String(error));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private collectRoutePaths(state: unknown, prefix: string[], paths: Set<string>): void {
|
|
91
|
+
for (const route of routeStatesIn(state)) {
|
|
92
|
+
if (typeof route.name === 'string') {
|
|
93
|
+
const path = [...prefix, route.name];
|
|
94
|
+
paths.add(path.join('/'));
|
|
95
|
+
this.collectRoutePaths(route.state, path, paths);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private navigateThroughFocusedScreens(route: string, params: UnknownRecord | null): HandlerResult {
|
|
101
|
+
const innermostFirst = [...this.sources.readFocusedScreens()].reverse();
|
|
102
|
+
const owner = innermostFirst.find((screen) => routeNamesOf(screen.navigation).includes(route));
|
|
103
|
+
if (owner === undefined) {
|
|
104
|
+
return failed('element_not_found', `route "${route}" is not reachable from the focused screens`);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
owner.navigation.navigate(...(params === null ? [route] : [route, params]));
|
|
108
|
+
return succeeded();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return failed('execution_error', error instanceof Error ? error.message : String(error));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private navigateThroughRef(route: string, params: UnknownRecord | null): HandlerResult | null {
|
|
115
|
+
const ref = this.sources.getNavigationRef();
|
|
116
|
+
if (ref === null || !ref.isReady() || !isFunction(ref.navigate)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const path = pathToRoute(ref.getRootState(), route);
|
|
120
|
+
if (path === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const [name, nested] = nestedNavigateArguments(path, params);
|
|
125
|
+
ref.navigate(...(nested === undefined ? [name] : [name, nested]));
|
|
126
|
+
return succeeded();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return failed('execution_error', error instanceof Error ? error.message : String(error));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isRecord, nonEmptyString } from '../fiber/narrow.ts';
|
|
2
|
+
import { type AgentAction, type AgentCommandData, type PageContentRequest, type RiskLevel, type ScrollDirectionName } from './types.ts';
|
|
3
|
+
|
|
4
|
+
const DIRECTIONS: readonly ScrollDirectionName[] = ['up', 'down', 'left', 'right'];
|
|
5
|
+
const RISK_LEVELS: readonly RiskLevel[] = ['safe', 'reversible', 'irreversible', 'destructive'];
|
|
6
|
+
const NO_LIMIT = 0;
|
|
7
|
+
|
|
8
|
+
function isDirection(value: unknown): value is ScrollDirectionName {
|
|
9
|
+
return DIRECTIONS.some((direction) => direction === value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseRiskLevel(value: unknown): RiskLevel {
|
|
13
|
+
return RISK_LEVELS.find((level) => level === value) ?? 'reversible';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parsePageContentRequest(options: unknown): PageContentRequest {
|
|
17
|
+
const record = isRecord(options) ? options : {};
|
|
18
|
+
return {
|
|
19
|
+
viewportOnly: record.viewport_only === true,
|
|
20
|
+
maxChars: typeof record.max_chars === 'number' ? record.max_chars : NO_LIMIT,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseAction(raw: unknown): AgentAction | null {
|
|
25
|
+
if (!isRecord(raw)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
switch (raw.type) {
|
|
29
|
+
case 'click':
|
|
30
|
+
return { type: 'click' };
|
|
31
|
+
case 'type':
|
|
32
|
+
return typeof raw.value === 'string' ? { type: 'type', value: raw.value, clear_first: raw.clear_first !== false } : null;
|
|
33
|
+
case 'check':
|
|
34
|
+
return { type: 'check', checked: typeof raw.checked === 'boolean' ? raw.checked : null };
|
|
35
|
+
case 'press_key':
|
|
36
|
+
return typeof raw.key === 'string' ? { type: 'press_key', key: raw.key } : null;
|
|
37
|
+
case 'scroll':
|
|
38
|
+
return isDirection(raw.direction)
|
|
39
|
+
? { type: 'scroll', direction: raw.direction, amount: typeof raw.amount === 'number' ? raw.amount : null }
|
|
40
|
+
: null;
|
|
41
|
+
case 'scroll_to':
|
|
42
|
+
return { type: 'scroll_to' };
|
|
43
|
+
case 'navigate': {
|
|
44
|
+
const route = nonEmptyString(raw.route) ?? nonEmptyString(raw.name) ?? nonEmptyString(raw.url);
|
|
45
|
+
return route === null ? null : { type: 'navigate', route, params: isRecord(raw.params) ? raw.params : null };
|
|
46
|
+
}
|
|
47
|
+
case 'highlight':
|
|
48
|
+
return { type: 'highlight' };
|
|
49
|
+
default:
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseAgentCommand(data: unknown): AgentCommandData | null {
|
|
55
|
+
if (!isRecord(data)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const commandId = nonEmptyString(data.command_id);
|
|
59
|
+
const action = parseAction(data.action);
|
|
60
|
+
if (commandId === null || action === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const agentId = isRecord(data.target) ? nonEmptyString(data.target.agent_id) : null;
|
|
64
|
+
return {
|
|
65
|
+
command_id: commandId,
|
|
66
|
+
action,
|
|
67
|
+
agent_id: agentId,
|
|
68
|
+
risk_level: parseRiskLevel(data.risk_level),
|
|
69
|
+
confirmation_required: data.confirmation_required === true,
|
|
70
|
+
teaching_explanation: nonEmptyString(data.teaching_explanation),
|
|
71
|
+
include_page_content: data.include_page_content === true,
|
|
72
|
+
page_content_options: parsePageContentRequest(data.page_content_options),
|
|
73
|
+
dom_settle_ms: typeof data.dom_settle_ms === 'number' && data.dom_settle_ms > 0 ? data.dom_settle_ms : 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function describeUnparsedAction(data: unknown): string {
|
|
78
|
+
if (!isRecord(data) || !isRecord(data.action)) {
|
|
79
|
+
return 'agent_command without an action';
|
|
80
|
+
}
|
|
81
|
+
return `unsupported or malformed action "${String(data.action.type)}"`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ActionErrorCode, FailedResult, HandlerResult } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function succeeded(message?: string): HandlerResult {
|
|
4
|
+
return message === undefined ? { ok: true } : { ok: true, message };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function failed(error: ActionErrorCode, message: string): FailedResult {
|
|
8
|
+
return { ok: false, error, message };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function failedFromException(error: unknown): FailedResult {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
return failed('execution_error', message);
|
|
14
|
+
}
|