@farmslot/adapter-rn 0.20.1 → 0.21.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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `@farmslot/adapter-rn` (published as `@farmslot/expo-reci
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - Active-development baseline; add user-facing changes here before release or package publication.
8
+
9
+ ## 0.21.0 - 2026-10-13
10
+
11
+ - Honor HUD top/bottom placement through automatic progress and capture the painted final outcome. Native HUD consumers need the optional `react-native-safe-area-context` peer.
12
+
7
13
  ## 0.20.1 - 2026-10-12
8
14
 
9
15
  - Update shared runtime dependencies for bounded recipe log observations.
package/README.md CHANGED
@@ -136,6 +136,10 @@ The generated bridge no-ops unless both conditions are true:
136
136
  - `__DEV__`
137
137
  - `EXPO_PUBLIC_FARMSLOT_RECIPE_BRIDGE=1`
138
138
 
139
+ Native HUD consumers install the optional `react-native-safe-area-context` peer.
140
+ The generated provider supplies that context and the shared renderer uses host
141
+ insets for top placement. CLI-only consumers need no native peers.
142
+
139
143
  The bridge and HUD are copied into local source files by design, so each app can customize `bridgeName`, bridge enablement, HUD text, HUD styles, or full HUD rendering without forking the harness.
140
144
 
141
145
  Default HUD text is compact and wraps instead of ellipsizing. To tune the generated HUD without replacing it:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farmslot/adapter-rn",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,15 +46,16 @@
46
46
  "prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/adapter-rn"
47
47
  },
48
48
  "dependencies": {
49
- "@farmslot/protocol": "0.40.0",
50
- "@farmslot/recipe-runner": "0.31.0",
49
+ "@farmslot/protocol": "0.41.0",
50
+ "@farmslot/recipe-runner": "0.32.0",
51
51
  "pngjs": "^7.0.0",
52
52
  "ws": "^8.18.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "agent-device": ">=0.19.3",
56
56
  "react": ">=19",
57
- "react-native": ">=0.79"
57
+ "react-native": ">=0.79",
58
+ "react-native-safe-area-context": ">=5"
58
59
  },
59
60
  "peerDependenciesMeta": {
60
61
  "agent-device": {
@@ -65,6 +66,9 @@
65
66
  },
66
67
  "react-native": {
67
68
  "optional": true
69
+ },
70
+ "react-native-safe-area-context": {
71
+ "optional": true
68
72
  }
69
73
  },
70
74
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  type StyleProp,
4
4
  StyleSheet,
@@ -8,8 +8,13 @@ import {
8
8
  type ViewProps,
9
9
  type ViewStyle,
10
10
  } from 'react-native';
11
+ import { initialWindowMetrics, SafeAreaInsetsContext } from 'react-native-safe-area-context';
11
12
 
12
- import { hudIntentDisplay } from '@farmslot/protocol/recipe';
13
+ import {
14
+ hudIntentDisplay,
15
+ type RecipeHudPlacement,
16
+ waitForHudPaint,
17
+ } from '@farmslot/protocol/recipe';
13
18
 
14
19
  export type FarmslotRecipeHudStatus = 'idle' | 'running' | 'pass' | 'fail';
15
20
 
@@ -19,6 +24,55 @@ export interface FarmslotRecipeHudState {
19
24
  currentStep?: number;
20
25
  totalSteps?: number;
21
26
  error?: string;
27
+ position?: RecipeHudPlacement;
28
+ }
29
+
30
+ export type RecipeHudStateController = readonly [
31
+ FarmslotRecipeHudState | null,
32
+ (state: FarmslotRecipeHudState | null) => Promise<void>,
33
+ ];
34
+
35
+ interface HudAcknowledgement {
36
+ resolve(): void;
37
+ reject(error: unknown): void;
38
+ }
39
+
40
+ /** Bridge acknowledgement follows the committed HUD rather than the state setter. */
41
+ export function useRecipeHudState(): RecipeHudStateController {
42
+ const [state, setState] = useState<FarmslotRecipeHudState | null>(null);
43
+ const current = useRef(state);
44
+ current.current = state;
45
+ const pending = useRef<HudAcknowledgement[]>([]);
46
+ const lifecycle = useRef<AbortController | null>(null);
47
+ const update = useCallback((next: FarmslotRecipeHudState | null) => {
48
+ if (next === current.current) return Promise.resolve();
49
+ return new Promise<void>((resolve, reject) => {
50
+ pending.current.push({ resolve, reject });
51
+ setState(next);
52
+ });
53
+ }, []);
54
+ useEffect(() => {
55
+ const controller = new AbortController();
56
+ lifecycle.current = controller;
57
+ return () => {
58
+ const error = new Error('HUD provider unmounted.');
59
+ controller.abort(error);
60
+ for (const acknowledgement of pending.current.splice(0)) acknowledgement.reject(error);
61
+ };
62
+ }, []);
63
+ useEffect(() => {
64
+ const acknowledgements = pending.current.splice(0);
65
+ if (acknowledgements.length)
66
+ void waitForHudPaint(requestAnimationFrame, lifecycle.current?.signal).then(
67
+ () => {
68
+ for (const acknowledgement of acknowledgements) acknowledgement.resolve();
69
+ },
70
+ (error: unknown) => {
71
+ for (const acknowledgement of acknowledgements) acknowledgement.reject(error);
72
+ },
73
+ );
74
+ }, [state]);
75
+ return [state, update];
22
76
  }
23
77
 
24
78
  export interface FarmslotRecipeHudText {
@@ -61,6 +115,7 @@ export function RecipeHud({
61
115
  overlay: Overlay,
62
116
  onLayout,
63
117
  }: Readonly<RecipeHudProps>): React.ReactElement | null {
118
+ const insets = useContext(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets;
64
119
  if (!state || state.status === 'idle') return null;
65
120
  const badge = text?.badge?.(state) ?? formatBadge(state);
66
121
  const intent = hudIntentDisplay(text?.intent?.(state) ?? state.intent, 'native');
@@ -75,7 +130,13 @@ export function RecipeHud({
75
130
  pointerEvents="none"
76
131
  style={[
77
132
  styles.container,
133
+ state.position === 'top' ? { top: (insets?.top ?? 0) + 16, bottom: undefined } : undefined,
78
134
  customStyles?.container,
135
+ state.position === 'top'
136
+ ? { bottom: undefined }
137
+ : state.position === 'bottom'
138
+ ? { top: undefined }
139
+ : undefined,
79
140
  intent.panel,
80
141
  { overflow: 'hidden', flexDirection: 'row' },
81
142
  ]}
@@ -2,6 +2,11 @@
2
2
 
3
3
  This directory was generated by `@farmslot/adapter-rn`.
4
4
 
5
+ Native bridge consumers install `react-native-safe-area-context` alongside their
6
+ React Native peers. The generated provider supplies its context to the shared
7
+ HUD so top captions respect status bars and cutouts. CLI-only consumers need no
8
+ native peers.
9
+
5
10
  The files use Recipe Protocol v1, but the project-facing path and scripts are intentionally versionless so future protocol upgrades do not churn app code.
6
11
 
7
12
  ## Quality rules
@@ -66,6 +66,16 @@
66
66
  "properties": {
67
67
  "clear": {
68
68
  "type": "boolean"
69
+ },
70
+ "display": {
71
+ "type": "object",
72
+ "properties": {
73
+ "position": {
74
+ "type": "string",
75
+ "enum": ["top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"]
76
+ }
77
+ },
78
+ "additionalProperties": true
69
79
  }
70
80
  },
71
81
  "additionalProperties": false
@@ -1,7 +1,15 @@
1
- import React, { createContext, useEffect, useMemo, useState } from 'react';
1
+ import React, { createContext, useEffect, useMemo } from 'react';
2
2
  import { Platform, View } from 'react-native';
3
+ import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-context';
3
4
 
4
- import { type FarmslotRecipeHudState, RecipeHud, type RecipeHudOptions } from './RecipeHud';
5
+ import { hudPlacement } from '@farmslot/protocol/recipe';
6
+
7
+ import {
8
+ type FarmslotRecipeHudState,
9
+ RecipeHud,
10
+ type RecipeHudOptions,
11
+ useRecipeHudState,
12
+ } from './RecipeHud';
5
13
 
6
14
  export interface FarmslotRecipeBridgeCommand {
7
15
  command: string;
@@ -38,7 +46,7 @@ export function RecipeBridgeProvider({
38
46
  renderHud,
39
47
  }: Readonly<RecipeBridgeProviderProps>): React.ReactElement {
40
48
  const enabled = isEnabled();
41
- const [hud, setHud] = useState<FarmslotRecipeHudState | null>(null);
49
+ const [hud, setHud] = useRecipeHudState();
42
50
 
43
51
  const bridge = useMemo<FarmslotRecipeBridgeApi>(
44
52
  () => ({
@@ -50,7 +58,7 @@ export function RecipeBridgeProvider({
50
58
  return { ok: true, bridge: bridgeName, platform: Platform.OS, hud: true };
51
59
  }
52
60
  if (command.command === 'hud') {
53
- setHud(normalizeHudState(command.payload));
61
+ await setHud(normalizeHudState(command.payload));
54
62
  return { ok: true };
55
63
  }
56
64
  if (command.command === 'trace') {
@@ -59,7 +67,7 @@ export function RecipeBridgeProvider({
59
67
  return { ok: false, error: `Unsupported Farmslot bridge command: ${command.command}` };
60
68
  },
61
69
  }),
62
- [bridgeName, enabled],
70
+ [bridgeName, enabled, setHud],
63
71
  );
64
72
 
65
73
  useEffect(() => {
@@ -76,10 +84,12 @@ export function RecipeBridgeProvider({
76
84
 
77
85
  return (
78
86
  <FarmslotRecipeBridgeContext.Provider value={bridge}>
79
- <View style={{ flex: 1 }}>
80
- {children}
81
- {renderHud ? renderHud(hud) : <RecipeHud state={hud} {...hudOptions} />}
82
- </View>
87
+ <SafeAreaProvider initialMetrics={initialWindowMetrics}>
88
+ <View style={{ flex: 1 }}>
89
+ {children}
90
+ {renderHud ? renderHud(hud) : <RecipeHud state={hud} {...hudOptions} />}
91
+ </View>
92
+ </SafeAreaProvider>
83
93
  </FarmslotRecipeBridgeContext.Provider>
84
94
  );
85
95
  }
@@ -102,6 +112,7 @@ function normalizeHudState(payload: Record<string, unknown> | undefined): Farmsl
102
112
  ),
103
113
  totalSteps: asOptionalNumber(payload?.totalSteps ?? payload?.total_steps ?? progress?.total),
104
114
  error: asOptionalString(payload?.error),
115
+ position: hudPlacement(payload?.display),
105
116
  };
106
117
  }
107
118
 
@@ -7,4 +7,4 @@ export type {
7
7
  RecipeHudOverlayProps,
8
8
  RecipeHudProps,
9
9
  } from '@farmslot/adapter-rn/recipe-hud';
10
- export { RecipeHud } from '@farmslot/adapter-rn/recipe-hud';
10
+ export { RecipeHud, useRecipeHudState } from '@farmslot/adapter-rn/recipe-hud';