@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,100 @@
|
|
|
1
|
+
import { isRecord, nonEmptyString, type UnknownRecord } from '../fiber/narrow.ts';
|
|
2
|
+
|
|
3
|
+
export interface NudgeSender {
|
|
4
|
+
name: string;
|
|
5
|
+
subtitle: string | null;
|
|
6
|
+
avatarUrl: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface NudgeProgress {
|
|
10
|
+
total: number;
|
|
11
|
+
completed: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NudgeView {
|
|
15
|
+
bubbleMessage: string;
|
|
16
|
+
continuation: boolean;
|
|
17
|
+
milestoneMessageId: number | null;
|
|
18
|
+
sessionId: number | null;
|
|
19
|
+
sender: NudgeSender | null;
|
|
20
|
+
progress: NudgeProgress | null;
|
|
21
|
+
ctaLabel: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface NudgeContext {
|
|
25
|
+
milestoneMessageId: number | null;
|
|
26
|
+
sessionId: number | null;
|
|
27
|
+
continuation: boolean;
|
|
28
|
+
ctaLabel: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function numberOrNull(value: unknown): number | null {
|
|
32
|
+
return typeof value === 'number' ? value : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readSender(raw: unknown): NudgeSender | null {
|
|
36
|
+
if (!isRecord(raw)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const name = nonEmptyString(raw.name);
|
|
40
|
+
return name === null ? null : { name, subtitle: nonEmptyString(raw.subtitle), avatarUrl: nonEmptyString(raw.avatar_url) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readProgress(raw: unknown): NudgeProgress | null {
|
|
44
|
+
if (!isRecord(raw) || typeof raw.total !== 'number' || typeof raw.completed !== 'number') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { total: raw.total, completed: raw.completed };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readNudge(wire: UnknownRecord, context: NudgeContext): NudgeView | null {
|
|
51
|
+
const bubbleMessage = nonEmptyString(wire.bubble_message);
|
|
52
|
+
if (bubbleMessage === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
bubbleMessage,
|
|
57
|
+
continuation: wire.bubble_kind === 'channel_continuation' || context.continuation,
|
|
58
|
+
milestoneMessageId: context.milestoneMessageId,
|
|
59
|
+
sessionId: context.sessionId,
|
|
60
|
+
sender: readSender(wire.bubble_sender),
|
|
61
|
+
progress: readProgress(wire.bubble_progress),
|
|
62
|
+
ctaLabel: nonEmptyString(wire.bubble_cta_label) ?? context.ctaLabel,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseHistoryNudge(frame: UnknownRecord): NudgeView | null {
|
|
67
|
+
return readNudge(frame, {
|
|
68
|
+
milestoneMessageId: numberOrNull(frame.milestone_message_id),
|
|
69
|
+
sessionId: numberOrNull(frame.session_id),
|
|
70
|
+
continuation: false,
|
|
71
|
+
ctaLabel: null,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseMessageNudge(frame: UnknownRecord, currentSessionId: number | null): NudgeView | null {
|
|
76
|
+
const message = isRecord(frame.message) ? frame.message : null;
|
|
77
|
+
if (message === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const metadata = isRecord(message.metadata) ? message.metadata : {};
|
|
81
|
+
return readNudge(message, {
|
|
82
|
+
milestoneMessageId: numberOrNull(metadata.milestone_message_id),
|
|
83
|
+
sessionId: currentSessionId,
|
|
84
|
+
continuation: metadata.widget_bubble_kind === 'channel_continuation',
|
|
85
|
+
ctaLabel: nonEmptyString(metadata.widget_bubble_cta_label),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseFrameNudge(frame: UnknownRecord, currentSessionId: number | null): NudgeView | null {
|
|
90
|
+
if (frame.type === 'history') {
|
|
91
|
+
return parseHistoryNudge(frame);
|
|
92
|
+
}
|
|
93
|
+
return frame.type === 'message' ? parseMessageNudge(frame, currentSessionId) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function initialsOf(name: string): string {
|
|
97
|
+
const parts = name.trim().split(/\s+/).filter((part) => part.length > 0);
|
|
98
|
+
const letters = parts.slice(0, 2).map((part) => part.charAt(0).toUpperCase());
|
|
99
|
+
return letters.join('');
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const require: (moduleName: string) => unknown;
|
|
2
|
+
|
|
3
|
+
function loadReactNativeHapticFeedback(): unknown {
|
|
4
|
+
try {
|
|
5
|
+
return require('react-native-haptic-feedback');
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const REACT_NATIVE_HAPTIC_FEEDBACK: unknown = loadReactNativeHapticFeedback();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const require: (moduleName: string) => unknown;
|
|
2
|
+
|
|
3
|
+
function loadReactNativeKeepAwake(): unknown {
|
|
4
|
+
try {
|
|
5
|
+
return require('react-native-keep-awake');
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const REACT_NATIVE_KEEP_AWAKE: unknown = loadReactNativeKeepAwake();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { AGENT_OVERLAY_TEST_ID } from '../snapshot/collect.ts';
|
|
5
|
+
|
|
6
|
+
export interface AgentOverlayProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AgentOverlay({ children }: AgentOverlayProps): React.JSX.Element {
|
|
11
|
+
return (
|
|
12
|
+
<View pointerEvents="box-none" testID={AGENT_OVERLAY_TEST_ID} style={StyleSheet.absoluteFill} collapsable={false}>
|
|
13
|
+
{children}
|
|
14
|
+
</View>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { ScreenshotOptions } from '../agent/frames.ts';
|
|
4
|
+
import type { CaptureBackend } from './capture-backend.ts';
|
|
5
|
+
import { SKIA_MODULE, skiaBackend } from './skia.ts';
|
|
6
|
+
import { VIEW_SHOT_BACKEND } from './view-shot.ts';
|
|
7
|
+
|
|
8
|
+
export type ScreenshotResult = { ok: true; dataUrl: string; capturesModals: boolean } | { ok: false; reason: string };
|
|
9
|
+
|
|
10
|
+
const SKIA_BACKEND = SKIA_MODULE === null ? null : skiaBackend(SKIA_MODULE, Platform.OS);
|
|
11
|
+
|
|
12
|
+
export const CAPTURE_BACKEND: CaptureBackend | null = VIEW_SHOT_BACKEND ?? SKIA_BACKEND;
|
|
13
|
+
|
|
14
|
+
export class Screenshotter {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly getCaptureTarget: () => unknown,
|
|
17
|
+
private readonly backend: CaptureBackend | null = CAPTURE_BACKEND,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
isAvailable(): boolean {
|
|
21
|
+
return this.backend !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async capture(options: ScreenshotOptions): Promise<ScreenshotResult> {
|
|
25
|
+
const backend = this.backend;
|
|
26
|
+
if (backend === null) {
|
|
27
|
+
return { ok: false, reason: 'no capture module: install react-native-view-shot (or @shopify/react-native-skia on iOS)' };
|
|
28
|
+
}
|
|
29
|
+
const target = this.getCaptureTarget();
|
|
30
|
+
if (target === null || target === undefined) {
|
|
31
|
+
return { ok: false, reason: 'the provider is not mounted' };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return { ok: true, dataUrl: await backend.capture(target, options), capturesModals: backend.capturesModals };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ScreenshotOptions } from '../agent/frames.ts';
|
|
2
|
+
import { isFunction, isRecord } from '../fiber/narrow.ts';
|
|
3
|
+
import type { CaptureBackend } from './capture-backend.ts';
|
|
4
|
+
|
|
5
|
+
interface SkiaImage {
|
|
6
|
+
width: () => number;
|
|
7
|
+
height: () => number;
|
|
8
|
+
encodeToBase64: (format: number, quality: number) => string;
|
|
9
|
+
dispose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SkiaCanvas {
|
|
13
|
+
drawImageRect: (image: SkiaImage, source: unknown, destination: unknown, paint: unknown) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SkiaSurface {
|
|
17
|
+
getCanvas: () => SkiaCanvas;
|
|
18
|
+
flush: () => void;
|
|
19
|
+
makeImageSnapshot: () => SkiaImage;
|
|
20
|
+
dispose: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SkiaModule {
|
|
24
|
+
makeImageFromView: (viewRef: { current: unknown }) => Promise<SkiaImage | null>;
|
|
25
|
+
ImageFormat: { JPEG: number };
|
|
26
|
+
Skia: {
|
|
27
|
+
Surface: { Make: (width: number, height: number) => SkiaSurface | null };
|
|
28
|
+
XYWHRect: (x: number, y: number, width: number, height: number) => unknown;
|
|
29
|
+
Paint: () => unknown;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare const require: (moduleName: string) => unknown;
|
|
34
|
+
|
|
35
|
+
export function isSkiaModule(value: unknown): value is SkiaModule {
|
|
36
|
+
return (
|
|
37
|
+
isRecord(value) &&
|
|
38
|
+
isFunction(value.makeImageFromView) &&
|
|
39
|
+
isRecord(value.ImageFormat) &&
|
|
40
|
+
typeof value.ImageFormat.JPEG === 'number' &&
|
|
41
|
+
isRecord(value.Skia) &&
|
|
42
|
+
isRecord(value.Skia.Surface) &&
|
|
43
|
+
isFunction(value.Skia.Surface.Make) &&
|
|
44
|
+
isFunction(value.Skia.XYWHRect) &&
|
|
45
|
+
isFunction(value.Skia.Paint)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadSkia(): SkiaModule | null {
|
|
50
|
+
try {
|
|
51
|
+
const loaded = require('@shopify/react-native-skia');
|
|
52
|
+
return isSkiaModule(loaded) ? loaded : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function encodeScaled(skia: SkiaModule, image: SkiaImage, options: ScreenshotOptions): string {
|
|
59
|
+
const quality = Math.round(options.quality * 100);
|
|
60
|
+
const width = image.width();
|
|
61
|
+
const height = image.height();
|
|
62
|
+
if (width <= options.maxWidth) {
|
|
63
|
+
return image.encodeToBase64(skia.ImageFormat.JPEG, quality);
|
|
64
|
+
}
|
|
65
|
+
const scaledHeight = Math.round((height * options.maxWidth) / width);
|
|
66
|
+
const surface = skia.Skia.Surface.Make(options.maxWidth, scaledHeight);
|
|
67
|
+
if (surface === null) {
|
|
68
|
+
return image.encodeToBase64(skia.ImageFormat.JPEG, quality);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
surface
|
|
72
|
+
.getCanvas()
|
|
73
|
+
.drawImageRect(image, skia.Skia.XYWHRect(0, 0, width, height), skia.Skia.XYWHRect(0, 0, options.maxWidth, scaledHeight), skia.Skia.Paint());
|
|
74
|
+
surface.flush();
|
|
75
|
+
const scaled = surface.makeImageSnapshot();
|
|
76
|
+
try {
|
|
77
|
+
return scaled.encodeToBase64(skia.ImageFormat.JPEG, quality);
|
|
78
|
+
} finally {
|
|
79
|
+
scaled.dispose();
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
surface.dispose();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function skiaBackend(skia: SkiaModule, platform: string): CaptureBackend | null {
|
|
87
|
+
if (platform !== 'ios') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
name: '@shopify/react-native-skia',
|
|
92
|
+
capturesModals: false,
|
|
93
|
+
capture: async (target, options) => {
|
|
94
|
+
const image = await skia.makeImageFromView({ current: target });
|
|
95
|
+
if (image === null) {
|
|
96
|
+
throw new Error('Skia could not capture the view');
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return `data:image/jpeg;base64,${encodeScaled(skia, image, options)}`;
|
|
100
|
+
} finally {
|
|
101
|
+
image.dispose();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const SKIA_MODULE: SkiaModule | null = loadSkia();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isFunction, isRecord } from '../fiber/narrow.ts';
|
|
2
|
+
import type { CaptureBackend } from './capture-backend.ts';
|
|
3
|
+
|
|
4
|
+
interface ViewShotOptions {
|
|
5
|
+
format: 'jpg';
|
|
6
|
+
quality: number;
|
|
7
|
+
result: 'data-uri';
|
|
8
|
+
width: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ViewShotModule {
|
|
12
|
+
captureRef: (target: unknown, options: ViewShotOptions) => Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare const require: (moduleName: string) => unknown;
|
|
16
|
+
|
|
17
|
+
function isViewShotModule(value: unknown): value is ViewShotModule {
|
|
18
|
+
return isRecord(value) && isFunction(value.captureRef);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadViewShot(): ViewShotModule | null {
|
|
22
|
+
try {
|
|
23
|
+
const loaded = require('react-native-view-shot');
|
|
24
|
+
return isViewShotModule(loaded) ? loaded : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function viewShotBackend(viewShot: ViewShotModule): CaptureBackend {
|
|
31
|
+
return {
|
|
32
|
+
name: 'react-native-view-shot',
|
|
33
|
+
capturesModals: false,
|
|
34
|
+
capture: (target, options) =>
|
|
35
|
+
viewShot.captureRef(target, { format: 'jpg', quality: options.quality, result: 'data-uri', width: options.maxWidth }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const VIEW_SHOT = loadViewShot();
|
|
40
|
+
|
|
41
|
+
export const VIEW_SHOT_BACKEND: CaptureBackend | null = VIEW_SHOT === null ? null : viewShotBackend(VIEW_SHOT);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { currentRootFiber } from '../fiber/locate.ts';
|
|
2
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
3
|
+
import { measureHost } from '../host/measure.ts';
|
|
4
|
+
import type { Size } from '../host/rect.ts';
|
|
5
|
+
import { type Candidate, collect, keepInnermostPerHost } from './collect.ts';
|
|
6
|
+
import type { ElementKind } from './kinds.ts';
|
|
7
|
+
import type { ScreenRef } from './navigation.ts';
|
|
8
|
+
import { describeRoutes, describeScreenPath } from './route-summary.ts';
|
|
9
|
+
import { ElementRegistry, type RegisteredElement } from './registry.ts';
|
|
10
|
+
import { countTwins, renderLine, renderSnapshotText, twinSignature } from './render.ts';
|
|
11
|
+
import { describeScrollDirections } from './scroll.ts';
|
|
12
|
+
import { groupIntoLines, limitCharacters, type PositionedText } from './text-layout.ts';
|
|
13
|
+
import { orderForReading } from './ordering.ts';
|
|
14
|
+
import { buildView } from './view.ts';
|
|
15
|
+
import { classifyVisibility, keepInTextTree, measureScrollGeometries, type ScrollGeometries } from './visibility.ts';
|
|
16
|
+
import { dropWrapperDuplicates, limitOffscreen, type MeasuredCandidate } from './wrappers.ts';
|
|
17
|
+
|
|
18
|
+
export interface Snapshot {
|
|
19
|
+
screen: string;
|
|
20
|
+
routes: string[];
|
|
21
|
+
text: string;
|
|
22
|
+
elements: RegisteredElement[];
|
|
23
|
+
modalOpen: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SnapshotDependencies {
|
|
27
|
+
getAnchorFiber: () => Fiber | null;
|
|
28
|
+
registry: ElementRegistry;
|
|
29
|
+
getViewport: () => Size;
|
|
30
|
+
getExtraRoutes: () => string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CaptureMode = 'register' | 'peek';
|
|
34
|
+
|
|
35
|
+
export interface TextTreeOptions {
|
|
36
|
+
viewportOnly: boolean;
|
|
37
|
+
maxChars: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TextTree {
|
|
41
|
+
screen: string;
|
|
42
|
+
text: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const EMPTY_SNAPSHOT: Snapshot = { screen: '', routes: [], text: renderSnapshotText({ screen: '', routes: [] }, []), elements: [], modalOpen: false };
|
|
46
|
+
|
|
47
|
+
export class SnapshotCapturer {
|
|
48
|
+
constructor(private readonly dependencies: SnapshotDependencies) {}
|
|
49
|
+
|
|
50
|
+
async capture(mode: CaptureMode): Promise<Snapshot> {
|
|
51
|
+
const anchor = this.dependencies.getAnchorFiber();
|
|
52
|
+
const root = anchor === null ? null : currentRootFiber(anchor);
|
|
53
|
+
if (root === null) {
|
|
54
|
+
return EMPTY_SNAPSHOT;
|
|
55
|
+
}
|
|
56
|
+
const { candidates, focusedScreens, modalOpen } = collect(root);
|
|
57
|
+
const kept = keepInnermostPerHost(candidates);
|
|
58
|
+
const geometries = await measureScrollGeometries(scrollContainersOf(kept));
|
|
59
|
+
const measured = await this.measureVisible(kept, geometries);
|
|
60
|
+
const ordered = orderForReading(dropWrapperDuplicates(measured));
|
|
61
|
+
return this.assemble(mode, ordered, geometries, { focusedScreens, modalOpen });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async captureText(options: TextTreeOptions): Promise<TextTree> {
|
|
65
|
+
const anchor = this.dependencies.getAnchorFiber();
|
|
66
|
+
const root = anchor === null ? null : currentRootFiber(anchor);
|
|
67
|
+
if (root === null) {
|
|
68
|
+
return { screen: '', text: '' };
|
|
69
|
+
}
|
|
70
|
+
const { textBlocks, focusedScreens } = collect(root);
|
|
71
|
+
const geometries = await measureScrollGeometries(textBlocks.flatMap((block) => block.scrollContainers));
|
|
72
|
+
const viewport = this.dependencies.getViewport();
|
|
73
|
+
const measured = await Promise.all(
|
|
74
|
+
textBlocks.map(async (block): Promise<PositionedText | null> => {
|
|
75
|
+
const rect = await measureHost(block.host);
|
|
76
|
+
if (rect === null || rect.width <= 0 || rect.height <= 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const visibility = classifyVisibility(rect, viewport, block.scrollContainers, geometries);
|
|
80
|
+
return keepInTextTree(visibility, rect, viewport, options.viewportOnly) ? { rect, text: block.text, heading: block.heading } : null;
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
const lines = limitCharacters(groupIntoLines(measured.filter((entry): entry is PositionedText => entry !== null)), options.maxChars);
|
|
84
|
+
const screen = describeScreenPath(focusedScreens);
|
|
85
|
+
return { screen, text: [`screen: ${screen}`, '', ...lines].join('\n') };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
readFocusedScreens(): ScreenRef[] {
|
|
89
|
+
const anchor = this.dependencies.getAnchorFiber();
|
|
90
|
+
const root = anchor === null ? null : currentRootFiber(anchor);
|
|
91
|
+
return root === null ? [] : collect(root).focusedScreens;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async measureVisible(kept: Candidate[], geometries: ScrollGeometries): Promise<MeasuredCandidate[]> {
|
|
95
|
+
const viewport = this.dependencies.getViewport();
|
|
96
|
+
const measured = await Promise.all(
|
|
97
|
+
kept.map(async (candidate): Promise<MeasuredCandidate | null> => {
|
|
98
|
+
const rect = await measureHost(candidate.host);
|
|
99
|
+
if (rect === null) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const visibility = classifyVisibility(rect, viewport, candidate.scrollContainers, geometries);
|
|
103
|
+
if (visibility.kind === 'hidden') {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const offscreen = visibility.kind === 'offscreen' ? visibility : null;
|
|
107
|
+
return { candidate, rect, offscreen: offscreen?.direction ?? null, distance: offscreen?.distance ?? 0 };
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
return limitOffscreen(measured.filter((entry): entry is MeasuredCandidate => entry !== null));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private assemble(
|
|
114
|
+
mode: CaptureMode,
|
|
115
|
+
ordered: MeasuredCandidate[],
|
|
116
|
+
geometries: ScrollGeometries,
|
|
117
|
+
scene: { focusedScreens: ScreenRef[]; modalOpen: boolean },
|
|
118
|
+
): Snapshot {
|
|
119
|
+
const views = ordered.map(({ candidate, rect, offscreen }) =>
|
|
120
|
+
buildView({
|
|
121
|
+
kind: candidate.kind,
|
|
122
|
+
fiber: candidate.fiber,
|
|
123
|
+
ancestors: candidate.ancestors,
|
|
124
|
+
rect,
|
|
125
|
+
scroll: candidate.kind === 'scroll' ? describeScrollDirections(geometries.get(candidate.fiber) ?? null) : null,
|
|
126
|
+
offscreen,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
if (mode === 'register') {
|
|
130
|
+
this.dependencies.registry.begin();
|
|
131
|
+
}
|
|
132
|
+
const twinCounts = countTwins(views);
|
|
133
|
+
const elements: RegisteredElement[] = [];
|
|
134
|
+
const lines = views.map((view, index) => {
|
|
135
|
+
const measuredEntry = ordered[index];
|
|
136
|
+
const twins = (twinCounts.get(twinSignature(view)) ?? 1) - 1;
|
|
137
|
+
const agentId = mode === 'register' && measuredEntry !== undefined ? this.register(view.kind, measuredEntry, elements) : null;
|
|
138
|
+
return renderLine(view, agentId, twins);
|
|
139
|
+
});
|
|
140
|
+
const header = {
|
|
141
|
+
screen: describeScreenPath(scene.focusedScreens),
|
|
142
|
+
routes: describeRoutes(scene.focusedScreens, this.dependencies.getExtraRoutes()),
|
|
143
|
+
};
|
|
144
|
+
return { ...header, text: renderSnapshotText(header, lines), elements, modalOpen: scene.modalOpen };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private register(kind: ElementKind, entry: MeasuredCandidate, elements: RegisteredElement[]): string {
|
|
148
|
+
const element = this.dependencies.registry.register(kind, entry.candidate.fiber, entry.rect);
|
|
149
|
+
elements.push(element);
|
|
150
|
+
return element.agentId;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function scrollContainersOf(kept: Candidate[]): Set<Fiber> {
|
|
155
|
+
const containers = new Set<Fiber>();
|
|
156
|
+
for (const candidate of kept) {
|
|
157
|
+
candidate.scrollContainers.forEach((container) => containers.add(container));
|
|
158
|
+
if (candidate.kind === 'scroll') {
|
|
159
|
+
containers.add(candidate.fiber);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return containers;
|
|
163
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isFunction, isRecord } from '../fiber/narrow.ts';
|
|
2
|
+
import { componentName, propsOf } from '../fiber/reader.ts';
|
|
3
|
+
import { type Fiber, FiberTag } from '../fiber/types.ts';
|
|
4
|
+
import type { ElementKind } from './kinds.ts';
|
|
5
|
+
|
|
6
|
+
export interface ScrollInstance {
|
|
7
|
+
scrollTo: (options: { x?: number; y?: number; animated?: boolean }) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function scrollInstanceOf(fiber: Fiber): ScrollInstance | null {
|
|
11
|
+
if (fiber.tag !== FiberTag.ClassComponent) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const instance = fiber.stateNode;
|
|
15
|
+
return isRecord(instance) && isFunction(instance.scrollTo) ? (instance as unknown as ScrollInstance) : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isTextInput(fiber: Fiber, props: Readonly<Record<string, unknown>>): boolean {
|
|
19
|
+
return isFunction(props.onChangeText) || componentName(fiber) === 'TextInput';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function classify(fiber: Fiber): ElementKind | null {
|
|
23
|
+
if (scrollInstanceOf(fiber) !== null) {
|
|
24
|
+
return 'scroll';
|
|
25
|
+
}
|
|
26
|
+
if (typeof fiber.type === 'string') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const props = propsOf(fiber);
|
|
30
|
+
if (isTextInput(fiber, props)) {
|
|
31
|
+
return 'text_input';
|
|
32
|
+
}
|
|
33
|
+
if (isFunction(props.onValueChange) && typeof props.value === 'boolean') {
|
|
34
|
+
return 'switch';
|
|
35
|
+
}
|
|
36
|
+
if (isFunction(props.onPress)) {
|
|
37
|
+
return 'button';
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|