@blario/react-native 0.1.6 → 0.1.7
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/README.md +4 -1
- package/dist/BlarProvider.d.ts +4 -0
- package/dist/telemetry/contain.d.ts +5 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/BlarProvider.tsx +55 -28
- package/src/agent-mode/keyboard-guard.ts +3 -1
- package/src/agent-mode/timers.ts +3 -1
- package/src/agent-mode/ui/AgentDock.tsx +8 -7
- package/src/agent-mode/ui/AgentLayer.tsx +2 -1
- package/src/agent-mode/ui/AgentLayerHost.tsx +2 -1
- package/src/agent-mode/ui/AgentPanel.tsx +4 -3
- package/src/agent-mode/ui/AgentTap.tsx +3 -2
- package/src/agent-mode/ui/SpotlightLayer.tsx +4 -3
- package/src/agent-mode/ui/StatusCard.tsx +3 -2
- package/src/agent-mode/ui/TouchShield.tsx +2 -1
- package/src/backend/foreground.ts +11 -6
- package/src/backend/health.ts +10 -6
- package/src/backend/presence.ts +12 -8
- package/src/backend/session-socket.ts +25 -18
- package/src/blar.ts +8 -7
- package/src/chat/ChatSheet.tsx +6 -5
- package/src/chat/ChatView.tsx +11 -9
- package/src/chat/Launcher.tsx +2 -1
- package/src/chat/widget-bundle.generated.ts +2 -2
- package/src/nudge/NudgeBubble.tsx +6 -5
- package/src/telemetry/contain.ts +28 -0
- package/src/telemetry/sdk-reporter.ts +3 -0
- package/src/transport/relay-transport.ts +13 -9
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -407,7 +407,10 @@ If trusted publishing is not wanted, a granular npm token (Read and write, scope
|
|
|
407
407
|
Like the web widget, the SDK reports its own contained failures to Blar's Sentry, through a client that is isolated from
|
|
408
408
|
the host app: it installs no global error handler and no native crash hook, never touches the app's own Sentry, and
|
|
409
409
|
only sees errors the SDK catches itself (the agent runtime, the backend transport and a boundary around the SDK's own
|
|
410
|
-
UI, never the app's components).
|
|
410
|
+
UI, never the app's components). An error in a socket callback, timer, native listener, animation callback or press
|
|
411
|
+
handler would otherwise end the app in a release build, or on Android's bridgeless runtime leave it on a white screen
|
|
412
|
+
with the process alive, so each goes through `contain()` (`src/telemetry/contain.ts`) and a test fails on one that
|
|
413
|
+
doesn't. A native crash can't be contained from JS and isn't reported here: it shows up in the app's own crash reporting. Events carry the SDK version, platform, publishable key, `source_id`, session id and
|
|
411
414
|
the user's external id; emails, bearer tokens and identity tokens are scrubbed from messages. Network failures and
|
|
412
415
|
refused credentials only become breadcrumbs, and at most 50 events are sent per launch. Without a DSN (the repo
|
|
413
416
|
default, set only by the release job) nothing is sent. `enabled={false}` stops the SDK, and its reporting with it.
|
package/dist/BlarProvider.d.ts
CHANGED
|
@@ -48,12 +48,16 @@ export declare class BlarProvider extends Component<BlarProviderProps, BlarProvi
|
|
|
48
48
|
private readonly stacking;
|
|
49
49
|
private readonly attachCursor;
|
|
50
50
|
private readonly detachCursor;
|
|
51
|
+
private readonly noteInteraction;
|
|
51
52
|
componentDidMount(): void;
|
|
52
53
|
componentDidUpdate(previous: BlarProviderProps): void;
|
|
53
54
|
componentWillUnmount(): void;
|
|
55
|
+
private startContained;
|
|
56
|
+
private syncProps;
|
|
54
57
|
private startSdk;
|
|
55
58
|
private stopSdk;
|
|
56
59
|
render(): React.JSX.Element;
|
|
60
|
+
private renderOverlay;
|
|
57
61
|
private renderAgentLayer;
|
|
58
62
|
private renderChat;
|
|
59
63
|
private chatOptions;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type Report = (context: string, error: unknown) => void;
|
|
2
|
+
export declare function setContainmentReporter(reporter: Report): void;
|
|
3
|
+
export declare function attempt<T>(context: string, run: () => T): T | undefined;
|
|
4
|
+
export declare function contain<Args extends unknown[]>(context: string, callback: (...args: Args) => unknown): (...args: Args) => void;
|
|
5
|
+
export {};
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.1.
|
|
1
|
+
export declare const SDK_VERSION = "0.1.7";
|
package/package.json
CHANGED
package/src/BlarProvider.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { Component, createRef, type ReactNode } from 'react';
|
|
2
|
-
import { AccessibilityInfo, AppState, Dimensions, Keyboard, StyleSheet, View } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, AppState, type AppStateStatus, Dimensions, Keyboard, StyleSheet, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { createAgentController } from './agent/compose.ts';
|
|
5
5
|
import { SYSTEM_CLOCK } from './actions/settle.ts';
|
|
@@ -31,7 +31,9 @@ import { visibleViewport } from './host/visible-area.ts';
|
|
|
31
31
|
import { AgentOverlay } from './overlay/AgentOverlay.tsx';
|
|
32
32
|
import { CAPTURE_BACKEND } from './screenshot/screenshotter.ts';
|
|
33
33
|
import { countOpenModals } from './snapshot/modals.ts';
|
|
34
|
+
import { attempt, contain } from './telemetry/contain.ts';
|
|
34
35
|
import { SdkErrorBoundary } from './telemetry/SdkErrorBoundary.tsx';
|
|
36
|
+
import { reportSdkError } from './telemetry/sdk-reporter.ts';
|
|
35
37
|
import { resolveConnectionOptions, type ConnectionConfig } from './transport/connection-options.ts';
|
|
36
38
|
import { RelayTransport } from './transport/relay-transport.ts';
|
|
37
39
|
import type { AgentTransport } from './transport/transport.ts';
|
|
@@ -118,17 +120,37 @@ export class BlarProvider extends Component<BlarProviderProps, BlarProviderState
|
|
|
118
120
|
|
|
119
121
|
private readonly detachCursor = (): void => this.cursor.detach();
|
|
120
122
|
|
|
123
|
+
private readonly noteInteraction = contain('BlarProvider.touch', () => this.backend?.noteInteraction());
|
|
124
|
+
|
|
121
125
|
override componentDidMount(): void {
|
|
122
126
|
if (this.props.enabled !== false) {
|
|
123
|
-
this.
|
|
127
|
+
this.startContained();
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
override componentDidUpdate(previous: BlarProviderProps): void {
|
|
132
|
+
attempt('BlarProvider.update', () => this.syncProps(previous));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override componentWillUnmount(): void {
|
|
136
|
+
attempt('BlarProvider.unmount', () => this.stopSdk());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A failed start leaves the host app running, without the SDK.
|
|
140
|
+
private startContained(): void {
|
|
141
|
+
try {
|
|
142
|
+
this.startSdk();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
reportSdkError('BlarProvider.start', error);
|
|
145
|
+
attempt('BlarProvider.stop', () => this.stopSdk());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private syncProps(previous: BlarProviderProps): void {
|
|
128
150
|
const enabled = this.props.enabled !== false;
|
|
129
151
|
if (enabled !== (previous.enabled !== false)) {
|
|
130
152
|
if (enabled) {
|
|
131
|
-
this.
|
|
153
|
+
this.startContained();
|
|
132
154
|
} else {
|
|
133
155
|
this.stopSdk();
|
|
134
156
|
this.setState({ chat: null, agentMode: null, spotlight: this.spotlight.getSnapshot(), cursor: this.cursor.getSnapshot() });
|
|
@@ -145,10 +167,6 @@ export class BlarProvider extends Component<BlarProviderProps, BlarProviderState
|
|
|
145
167
|
void Blar.setUser(this.props.user);
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
override componentWillUnmount(): void {
|
|
149
|
-
this.stopSdk();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
170
|
private startSdk(): void {
|
|
153
171
|
this.connection = resolveConnectionOptions(this.props, typeof __DEV__ !== 'undefined' && __DEV__);
|
|
154
172
|
const transport = this.createTransport();
|
|
@@ -204,25 +222,29 @@ export class BlarProvider extends Component<BlarProviderProps, BlarProviderState
|
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
override render(): React.JSX.Element {
|
|
207
|
-
const strings = resolveStrings(this.props.language ?? 'es', this.props.strings);
|
|
208
|
-
const phase = this.state.agentMode?.phase ?? 'idle';
|
|
209
225
|
return (
|
|
210
|
-
<View style={styles.root} collapsable={false} onTouchStart={
|
|
226
|
+
<View style={styles.root} collapsable={false} onTouchStart={this.noteInteraction}>
|
|
211
227
|
<View ref={this.contentRef} style={styles.root} collapsable={false}>
|
|
212
228
|
{this.props.children}
|
|
213
229
|
</View>
|
|
214
|
-
<SdkErrorBoundary context="BlarProvider.overlay">
|
|
215
|
-
<AgentOverlay>
|
|
216
|
-
{this.renderChat(strings)}
|
|
217
|
-
<AgentLayerHost visible={phase !== 'idle'} generation={this.state.layerGeneration} onRequestClose={() => this.agentMode?.stop()}>
|
|
218
|
-
{this.renderAgentLayer(strings)}
|
|
219
|
-
</AgentLayerHost>
|
|
220
|
-
</AgentOverlay>
|
|
221
|
-
</SdkErrorBoundary>
|
|
230
|
+
<SdkErrorBoundary context="BlarProvider.overlay">{attempt('BlarProvider.render', () => this.renderOverlay())}</SdkErrorBoundary>
|
|
222
231
|
</View>
|
|
223
232
|
);
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
private renderOverlay(): React.JSX.Element {
|
|
236
|
+
const strings = resolveStrings(this.props.language ?? 'es', this.props.strings);
|
|
237
|
+
const phase = this.state.agentMode?.phase ?? 'idle';
|
|
238
|
+
return (
|
|
239
|
+
<AgentOverlay>
|
|
240
|
+
{this.renderChat(strings)}
|
|
241
|
+
<AgentLayerHost visible={phase !== 'idle'} generation={this.state.layerGeneration} onRequestClose={() => this.agentMode?.stop()}>
|
|
242
|
+
{this.renderAgentLayer(strings)}
|
|
243
|
+
</AgentLayerHost>
|
|
244
|
+
</AgentOverlay>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
226
248
|
private renderAgentLayer(strings: UiStrings): React.JSX.Element | null {
|
|
227
249
|
const { agentMode } = this.state;
|
|
228
250
|
if (agentMode === null || agentMode.phase === 'idle') {
|
|
@@ -280,17 +302,20 @@ export class BlarProvider extends Component<BlarProviderProps, BlarProviderState
|
|
|
280
302
|
}
|
|
281
303
|
|
|
282
304
|
private watchForeground(): void {
|
|
283
|
-
const subscription = AppState.addEventListener(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
305
|
+
const subscription = AppState.addEventListener(
|
|
306
|
+
'change',
|
|
307
|
+
contain('BlarProvider.foreground', (state: AppStateStatus) => {
|
|
308
|
+
if (state !== 'active') {
|
|
309
|
+
this.spotlight.backgrounded();
|
|
310
|
+
}
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
288
313
|
this.releases.push(() => subscription.remove());
|
|
289
314
|
}
|
|
290
315
|
|
|
291
316
|
private watchMotionPreference(): void {
|
|
292
317
|
void AccessibilityInfo.isReduceMotionEnabled().then((reduceMotion) => this.setState({ reduceMotion }));
|
|
293
|
-
const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', (reduceMotion) => this.setState({ reduceMotion }));
|
|
318
|
+
const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', contain('BlarProvider.motion', (reduceMotion: boolean) => this.setState({ reduceMotion })));
|
|
294
319
|
this.releases.push(() => subscription.remove());
|
|
295
320
|
}
|
|
296
321
|
|
|
@@ -413,10 +438,12 @@ export class BlarProvider extends Component<BlarProviderProps, BlarProviderState
|
|
|
413
438
|
return;
|
|
414
439
|
}
|
|
415
440
|
this.stackingCheckQueued = true;
|
|
416
|
-
requestAnimationFrame(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
441
|
+
requestAnimationFrame(
|
|
442
|
+
contain('BlarProvider.stacking', () => {
|
|
443
|
+
this.stackingCheckQueued = false;
|
|
444
|
+
this.keepLayerOnTop();
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
420
447
|
}
|
|
421
448
|
|
|
422
449
|
private keepLayerOnTop(): void {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { contain } from '../telemetry/contain.ts';
|
|
2
|
+
|
|
1
3
|
export interface KeyboardPort {
|
|
2
4
|
addListener: (event: 'keyboardWillShow' | 'keyboardDidShow', listener: () => void) => { remove: () => void };
|
|
3
5
|
dismiss: () => void;
|
|
@@ -26,7 +28,7 @@ export class KeyboardGuard {
|
|
|
26
28
|
if (this.subscriptions.length > 0) {
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
|
-
const dismiss = ()
|
|
31
|
+
const dismiss = contain('KeyboardGuard.dismiss', () => this.keyboard.dismiss());
|
|
30
32
|
this.subscriptions = [this.keyboard.addListener('keyboardWillShow', dismiss), this.keyboard.addListener('keyboardDidShow', dismiss)];
|
|
31
33
|
dismiss();
|
|
32
34
|
}
|
package/src/agent-mode/timers.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { contain } from '../telemetry/contain.ts';
|
|
2
|
+
|
|
1
3
|
export interface Timers {
|
|
2
4
|
set: (callback: () => void, milliseconds: number) => unknown;
|
|
3
5
|
clear: (handle: unknown) => void;
|
|
@@ -5,7 +7,7 @@ export interface Timers {
|
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export const SYSTEM_TIMERS: Timers = {
|
|
8
|
-
set: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
10
|
+
set: (callback, milliseconds) => setTimeout(contain('Timers', callback), milliseconds),
|
|
9
11
|
clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
10
12
|
now: () => Date.now(),
|
|
11
13
|
};
|
|
@@ -7,6 +7,7 @@ import { chooseDock, freeBand, messageCap, type PanelDock, STACK_GAP } from '../
|
|
|
7
7
|
import type { SpotlightSnapshot } from '../spotlight-controller.ts';
|
|
8
8
|
import { type ActiveSpotlight, AgentPanel, panelChrome } from './AgentPanel.tsx';
|
|
9
9
|
import { StatusCard, type StatusCorners } from './StatusCard.tsx';
|
|
10
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
10
11
|
|
|
11
12
|
export interface DockActions {
|
|
12
13
|
place: (id: number, band: Band) => void | Promise<void>;
|
|
@@ -88,7 +89,7 @@ export function AgentDock({ spotlight, actions, card, strings, color, reduceMoti
|
|
|
88
89
|
const pending = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
|
89
90
|
|
|
90
91
|
const later = (callback: () => void, milliseconds: number): void => {
|
|
91
|
-
pending.current.push(setTimeout(callback, milliseconds));
|
|
92
|
+
pending.current.push(setTimeout(contain('AgentDock.later', callback), milliseconds));
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
const rest = (): void => {
|
|
@@ -118,15 +119,15 @@ export function AgentDock({ spotlight, actions, card, strings, color, reduceMoti
|
|
|
118
119
|
const duration = reduceMotion ? REDUCED_MS : CLOSE_MS;
|
|
119
120
|
const easing = reduceMotion ? Easing.linear : CLOSE;
|
|
120
121
|
if (dockRef.current === 'bottom') {
|
|
121
|
-
Animated.parallel([animate(panelOpacity, 0, duration, easing), animate(panelShift, reduceMotion ? 0 : ENTER_SHIFT, duration, easing)]).start(() => {
|
|
122
|
+
Animated.parallel([animate(panelOpacity, 0, duration, easing), animate(panelShift, reduceMotion ? 0 : ENTER_SHIFT, duration, easing)]).start(contain('AgentDock.close', () => {
|
|
122
123
|
if (presentedId.current === null) {
|
|
123
124
|
rest();
|
|
124
125
|
setPresented(null);
|
|
125
126
|
}
|
|
126
|
-
});
|
|
127
|
+
}));
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
|
-
Animated.parallel([animate(stackOpacity, 0, duration, easing), animate(stackShift, reduceMotion ? 0 : -ENTER_SHIFT, duration, easing)]).start(() => {
|
|
130
|
+
Animated.parallel([animate(stackOpacity, 0, duration, easing), animate(stackShift, reduceMotion ? 0 : -ENTER_SHIFT, duration, easing)]).start(contain('AgentDock.closeTop', () => {
|
|
130
131
|
if (presentedId.current !== null) {
|
|
131
132
|
return;
|
|
132
133
|
}
|
|
@@ -134,7 +135,7 @@ export function AgentDock({ spotlight, actions, card, strings, color, reduceMoti
|
|
|
134
135
|
stackOpacity.setValue(0);
|
|
135
136
|
setPresented(null);
|
|
136
137
|
later(() => animate(stackOpacity, 1, STACK_RETURN_MS, Easing.out(Easing.ease)).start(), STACK_RETURN_PAUSE_MS);
|
|
137
|
-
});
|
|
138
|
+
}));
|
|
138
139
|
};
|
|
139
140
|
|
|
140
141
|
useEffect(() => {
|
|
@@ -193,7 +194,7 @@ export function AgentDock({ spotlight, actions, card, strings, color, reduceMoti
|
|
|
193
194
|
Animated.parallel([animate(panelOpacity, 1, duration, easing), animate(panelShift, 0, duration, easing)]).start();
|
|
194
195
|
return;
|
|
195
196
|
}
|
|
196
|
-
animate(stackOpacity, 0, STACK_FADE_MS, Easing.out(Easing.ease)).start(() => {
|
|
197
|
+
animate(stackOpacity, 0, STACK_FADE_MS, Easing.out(Easing.ease)).start(contain('AgentDock.moveTop', () => {
|
|
197
198
|
later(() => {
|
|
198
199
|
if (presentedId.current !== presented.id) {
|
|
199
200
|
return;
|
|
@@ -205,7 +206,7 @@ export function AgentDock({ spotlight, actions, card, strings, color, reduceMoti
|
|
|
205
206
|
stackShift.setValue(reduceMotion ? 0 : -ENTER_SHIFT);
|
|
206
207
|
Animated.parallel([animate(stackOpacity, 1, duration, easing), animate(stackShift, 0, duration, easing)]).start();
|
|
207
208
|
}, STACK_SWAP_PAUSE_MS);
|
|
208
|
-
});
|
|
209
|
+
}));
|
|
209
210
|
}, [presented]);
|
|
210
211
|
|
|
211
212
|
useEffect(() => {
|
|
@@ -14,6 +14,7 @@ import { MessageBubbles } from './MessageBubbles.tsx';
|
|
|
14
14
|
import { PendingOverlay } from './PendingOverlay.tsx';
|
|
15
15
|
import { SpotlightLayer } from './SpotlightLayer.tsx';
|
|
16
16
|
import { TouchShield } from './TouchShield.tsx';
|
|
17
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
17
18
|
|
|
18
19
|
export interface AgentLayerProps {
|
|
19
20
|
snapshot: AgentModeSnapshot;
|
|
@@ -64,7 +65,7 @@ function useBlockedTap(): { toast: boolean; pulse: number; block: () => void } {
|
|
|
64
65
|
if (timer.current !== null) {
|
|
65
66
|
clearTimeout(timer.current);
|
|
66
67
|
}
|
|
67
|
-
timer.current = setTimeout(() => setToast(false), TOAST_MS);
|
|
68
|
+
timer.current = setTimeout(contain('AgentLayer.toast', () => setToast(false)), TOAST_MS);
|
|
68
69
|
};
|
|
69
70
|
return { toast, pulse, block };
|
|
70
71
|
}
|
|
@@ -3,6 +3,7 @@ import { Modal, Platform, StyleSheet, View } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import { loadFullWindowOverlay } from './full-window-overlay.ts';
|
|
5
5
|
import { chooseLayerHost } from './layer-host.ts';
|
|
6
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
6
7
|
|
|
7
8
|
export interface AgentLayerHostProps {
|
|
8
9
|
visible: boolean;
|
|
@@ -20,7 +21,7 @@ export function AgentLayerHost({ visible, generation, onRequestClose, children }
|
|
|
20
21
|
}
|
|
21
22
|
if (HOST === 'modal') {
|
|
22
23
|
return (
|
|
23
|
-
<Modal key={generation} visible transparent animationType="none" hardwareAccelerated onRequestClose={onRequestClose}>
|
|
24
|
+
<Modal key={generation} visible transparent animationType="none" hardwareAccelerated onRequestClose={contain('AgentLayerHost.close', onRequestClose)}>
|
|
24
25
|
{children}
|
|
25
26
|
</Modal>
|
|
26
27
|
);
|
|
@@ -9,6 +9,7 @@ import type { PanelDock } from '../panel-geometry.ts';
|
|
|
9
9
|
import type { SpotlightSnapshot } from '../spotlight-controller.ts';
|
|
10
10
|
import { MarkdownText } from './MarkdownText.tsx';
|
|
11
11
|
import { WarningIcon } from './WarningIcon.tsx';
|
|
12
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
12
13
|
|
|
13
14
|
export type ActiveSpotlight = Exclude<SpotlightSnapshot, { kind: 'none' }>;
|
|
14
15
|
|
|
@@ -104,7 +105,7 @@ export function AgentPanel({ spotlight, dock, strings, color, messageMaxHeight,
|
|
|
104
105
|
{spotlight.kind === 'reading' ? (
|
|
105
106
|
<View style={styles.progressRow}>
|
|
106
107
|
<Progress durationMs={spotlight.durationMs} running={spotlight.shown} color={color} />
|
|
107
|
-
<Pressable accessibilityRole="button" onPress={onSkip} style={styles.skip}>
|
|
108
|
+
<Pressable accessibilityRole="button" onPress={contain('AgentPanel.skip', onSkip)} style={styles.skip}>
|
|
108
109
|
<Text style={styles.skipText}>{strings.readingSkip}</Text>
|
|
109
110
|
</Pressable>
|
|
110
111
|
</View>
|
|
@@ -117,12 +118,12 @@ export function AgentPanel({ spotlight, dock, strings, color, messageMaxHeight,
|
|
|
117
118
|
</View>
|
|
118
119
|
) : null}
|
|
119
120
|
<View style={[styles.buttons, destructive ? styles.destructiveButtons : null]}>
|
|
120
|
-
<Pressable accessibilityRole="button" onPress={onStop} style={[styles.button, styles.stop]}>
|
|
121
|
+
<Pressable accessibilityRole="button" onPress={contain('AgentPanel.stop', onStop)} style={[styles.button, styles.stop]}>
|
|
121
122
|
<Text style={[styles.buttonText, { color: INK }]}>{strings.confirmStop}</Text>
|
|
122
123
|
</Pressable>
|
|
123
124
|
<Pressable
|
|
124
125
|
accessibilityRole="button"
|
|
125
|
-
onPress={onConfirm}
|
|
126
|
+
onPress={contain('AgentPanel.confirm', onConfirm)}
|
|
126
127
|
style={[styles.button, destructive ? { backgroundColor: DESTRUCTIVE } : [styles.confirmButton, { backgroundColor: color }]]}
|
|
127
128
|
>
|
|
128
129
|
<Text style={[styles.buttonText, { color: destructive ? '#ffffff' : onColor(color) }]}>
|
|
@@ -3,6 +3,7 @@ import { Animated, Easing, StyleSheet, View } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import { centerOf, type Point } from '../../host/rect.ts';
|
|
5
5
|
import type { CursorSnapshot } from '../cursor.ts';
|
|
6
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
6
7
|
|
|
7
8
|
export interface AgentTapProps {
|
|
8
9
|
snapshot: CursorSnapshot;
|
|
@@ -90,7 +91,7 @@ function TapRing({ point, landed, ending, color, reduceMotion, onEnded }: TapRin
|
|
|
90
91
|
|
|
91
92
|
useEffect(() => {
|
|
92
93
|
if (ending) {
|
|
93
|
-
timing(opacity, 0, END_FADE_MS, EASE_OUT).start(() => ended.current());
|
|
94
|
+
timing(opacity, 0, END_FADE_MS, EASE_OUT).start(contain('AgentTap.ended', () => ended.current()));
|
|
94
95
|
}
|
|
95
96
|
}, [ending, opacity, ended]);
|
|
96
97
|
|
|
@@ -107,7 +108,7 @@ function TapRipple({ point, onDone }: { point: Point; onDone: () => void }): Rea
|
|
|
107
108
|
const progress = useRef(new Animated.Value(0)).current;
|
|
108
109
|
const done = useLatest(onDone);
|
|
109
110
|
useEffect(() => {
|
|
110
|
-
timing(progress, 1, RIPPLE_MS, RIPPLE, PRESS_MS).start(() => done.current());
|
|
111
|
+
timing(progress, 1, RIPPLE_MS, RIPPLE, PRESS_MS).start(contain('AgentTap.ripple', () => done.current()));
|
|
111
112
|
}, [progress, done]);
|
|
112
113
|
return (
|
|
113
114
|
<Animated.View
|
|
@@ -5,6 +5,7 @@ import type { SpotlightSnapshot } from '../spotlight-controller.ts';
|
|
|
5
5
|
import type { ActiveSpotlight } from './AgentPanel.tsx';
|
|
6
6
|
import { SPOTLIGHT_DIM, SpotlightOverlay } from './SpotlightOverlay.tsx';
|
|
7
7
|
import { TargetPulse } from './TargetPulse.tsx';
|
|
8
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
8
9
|
|
|
9
10
|
export interface SpotlightLayerProps {
|
|
10
11
|
spotlight: SpotlightSnapshot;
|
|
@@ -33,11 +34,11 @@ export function SpotlightLayer({ spotlight, color, reduceMotion, onBackdropPress
|
|
|
33
34
|
Animated.timing(opacity, { toValue: 1, duration, easing: Easing.out(Easing.ease), useNativeDriver: true }).start();
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
|
-
Animated.timing(opacity, { toValue: 0, duration, easing: Easing.out(Easing.ease), useNativeDriver: true }).start(({ finished }) => {
|
|
37
|
+
Animated.timing(opacity, { toValue: 0, duration, easing: Easing.out(Easing.ease), useNativeDriver: true }).start(contain('SpotlightLayer.fade', ({ finished }: { finished: boolean }) => {
|
|
37
38
|
if (finished) {
|
|
38
39
|
setGone(true);
|
|
39
40
|
}
|
|
40
|
-
});
|
|
41
|
+
}));
|
|
41
42
|
}, [visible, reduceMotion, opacity]);
|
|
42
43
|
|
|
43
44
|
const current = last.current;
|
|
@@ -46,7 +47,7 @@ export function SpotlightLayer({ spotlight, color, reduceMotion, onBackdropPress
|
|
|
46
47
|
}
|
|
47
48
|
return (
|
|
48
49
|
<Animated.View pointerEvents={visible ? 'box-none' : 'none'} style={[StyleSheet.absoluteFill, { opacity }]}>
|
|
49
|
-
{current.kind === 'confirming' ? <Pressable style={StyleSheet.absoluteFill} onPress={onBackdropPress} /> : null}
|
|
50
|
+
{current.kind === 'confirming' ? <Pressable style={StyleSheet.absoluteFill} onPress={contain('SpotlightLayer.backdrop', onBackdropPress)} /> : null}
|
|
50
51
|
{current.hole === null ? <View pointerEvents="none" style={styles.dim} /> : <SpotlightOverlay hole={current.hole} />}
|
|
51
52
|
{current.kind === 'confirming' && current.target !== null ? (
|
|
52
53
|
<TargetPulse target={current.target} radius={current.request.target?.radius ?? 0} color={color} reduceMotion={reduceMotion} />
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { onColor, withAlpha } from '../colors.ts';
|
|
5
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
5
6
|
|
|
6
7
|
export interface StatusCardProps {
|
|
7
8
|
status: string;
|
|
@@ -54,7 +55,7 @@ function useStopPulse(pulse: number): { scale: Animated.Value; bordered: boolean
|
|
|
54
55
|
]);
|
|
55
56
|
const animation = Animated.loop(beat, { iterations: PULSE_TIMES });
|
|
56
57
|
animation.start();
|
|
57
|
-
const timer = setTimeout(() => setBordered(false), BORDER_MS);
|
|
58
|
+
const timer = setTimeout(contain('StatusCard.border', () => setBordered(false)), BORDER_MS);
|
|
58
59
|
return () => {
|
|
59
60
|
animation.stop();
|
|
60
61
|
clearTimeout(timer);
|
|
@@ -83,7 +84,7 @@ export function StatusCard({ status, queued, stopLabel, color, stopHidden, corne
|
|
|
83
84
|
accessibilityLabel={stopLabel}
|
|
84
85
|
hitSlop={STOP_HIT_SLOP}
|
|
85
86
|
style={[styles.stop, { backgroundColor: withAlpha(ink, 0.2) }, bordered ? { borderWidth: 2, borderColor: withAlpha(ink, 0.8) } : null]}
|
|
86
|
-
onPress={onStop}
|
|
87
|
+
onPress={contain('StatusCard.stop', onStop)}
|
|
87
88
|
>
|
|
88
89
|
<View style={[styles.stopIcon, { backgroundColor: ink }]} />
|
|
89
90
|
</Pressable>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { contain } from '../../telemetry/contain.ts';
|
|
3
4
|
|
|
4
5
|
export interface TouchShieldProps {
|
|
5
6
|
onBlockedTap: () => void;
|
|
@@ -10,7 +11,7 @@ export function TouchShield({ onBlockedTap }: TouchShieldProps): React.JSX.Eleme
|
|
|
10
11
|
<View
|
|
11
12
|
style={styles.shield}
|
|
12
13
|
onStartShouldSetResponder={() => true}
|
|
13
|
-
onResponderRelease={onBlockedTap}
|
|
14
|
+
onResponderRelease={contain('TouchShield.tap', onBlockedTap)}
|
|
14
15
|
accessibilityElementsHidden
|
|
15
16
|
importantForAccessibility="no-hide-descendants"
|
|
16
17
|
/>
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { AppState } from 'react-native';
|
|
1
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { contain } from '../telemetry/contain.ts';
|
|
2
4
|
|
|
3
5
|
export function watchForeground(onActive: () => void): () => void {
|
|
4
|
-
const subscription = AppState.addEventListener(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const subscription = AppState.addEventListener(
|
|
7
|
+
'change',
|
|
8
|
+
contain('watchForeground', (state: AppStateStatus) => {
|
|
9
|
+
if (state === 'active') {
|
|
10
|
+
onActive();
|
|
11
|
+
}
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
9
14
|
return () => subscription.remove();
|
|
10
15
|
}
|
package/src/backend/health.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AppState, type AppStateStatus, type NativeEventSubscription } from 'react-native';
|
|
2
2
|
|
|
3
3
|
import { SDK_VERSION } from '../version.ts';
|
|
4
|
+
import { contain } from '../telemetry/contain.ts';
|
|
4
5
|
|
|
5
6
|
export interface ClientHealth {
|
|
6
7
|
visibility: 'visible' | 'hidden';
|
|
@@ -27,13 +28,16 @@ export class ClientHealthProbe {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
this.hiddenSince = AppState.currentState === 'active' ? null : Date.now();
|
|
30
|
-
this.subscription = AppState.addEventListener('change', (state) => this.onStateChange(state));
|
|
31
|
+
this.subscription = AppState.addEventListener('change', contain('ClientHealth.state', (state: AppStateStatus) => this.onStateChange(state)));
|
|
31
32
|
this.expectedTickAt = Date.now() + PROBE_INTERVAL_MS;
|
|
32
|
-
this.timer = setInterval(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
this.timer = setInterval(
|
|
34
|
+
contain('ClientHealth.probe', () => {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
this.worstLagMs = Math.max(this.worstLagMs, now - this.expectedTickAt);
|
|
37
|
+
this.expectedTickAt = now + PROBE_INTERVAL_MS;
|
|
38
|
+
}),
|
|
39
|
+
PROBE_INTERVAL_MS,
|
|
40
|
+
);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
stop(): void {
|
package/src/backend/presence.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AppState, type NativeEventSubscription } from 'react-native';
|
|
1
|
+
import { AppState, type AppStateStatus, type NativeEventSubscription } from 'react-native';
|
|
2
2
|
|
|
3
3
|
import type { ApiClient } from './api-client.ts';
|
|
4
|
+
import { contain } from '../telemetry/contain.ts';
|
|
4
5
|
|
|
5
6
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
6
7
|
const IDLE_TIMEOUT_MS = 3 * 60_000;
|
|
@@ -29,12 +30,15 @@ export class PresenceManager {
|
|
|
29
30
|
}
|
|
30
31
|
this.started = true;
|
|
31
32
|
this.lastInteraction = this.now();
|
|
32
|
-
this.subscription = AppState.addEventListener(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
this.subscription = AppState.addEventListener(
|
|
34
|
+
'change',
|
|
35
|
+
contain('Presence.state', (state: AppStateStatus) => {
|
|
36
|
+
if (state === 'active') {
|
|
37
|
+
this.lastInteraction = this.now();
|
|
38
|
+
this.fireNow();
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
38
42
|
this.fireNow();
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -71,7 +75,7 @@ export class PresenceManager {
|
|
|
71
75
|
if (this.timer !== null) {
|
|
72
76
|
clearTimeout(this.timer);
|
|
73
77
|
}
|
|
74
|
-
this.timer = setTimeout(() => this.fireNow(), HEARTBEAT_INTERVAL_MS);
|
|
78
|
+
this.timer = setTimeout(contain('Presence.beat', () => this.fireNow()), HEARTBEAT_INTERVAL_MS);
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
private async send(): Promise<void> {
|
|
@@ -3,6 +3,7 @@ import { backoffDelay, isExhausted, SOCKET_BACKOFF } from './backoff.ts';
|
|
|
3
3
|
import type { IdentityProof } from './identity.ts';
|
|
4
4
|
import { ResponseCache } from './response-cache.ts';
|
|
5
5
|
import { buildSocketUrl } from './socket-url.ts';
|
|
6
|
+
import { contain } from '../telemetry/contain.ts';
|
|
6
7
|
|
|
7
8
|
export type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed';
|
|
8
9
|
|
|
@@ -149,7 +150,7 @@ export class SessionSocket {
|
|
|
149
150
|
return;
|
|
150
151
|
}
|
|
151
152
|
this.socket = socket;
|
|
152
|
-
socket.onopen = () => {
|
|
153
|
+
socket.onopen = contain('SessionSocket.open', () => {
|
|
153
154
|
if (this.epoch !== epoch) {
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
@@ -159,8 +160,8 @@ export class SessionSocket {
|
|
|
159
160
|
this.options.onState('connected');
|
|
160
161
|
socket.send(JSON.stringify({ type: 'joined' }));
|
|
161
162
|
this.cache.drain().forEach((payload) => socket.send(payload));
|
|
162
|
-
};
|
|
163
|
-
socket.onmessage = (event) => {
|
|
163
|
+
});
|
|
164
|
+
socket.onmessage = contain('SessionSocket.message', (event: { data?: unknown }) => {
|
|
164
165
|
if (this.epoch !== epoch || typeof event.data !== 'string') {
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
@@ -170,13 +171,13 @@ export class SessionSocket {
|
|
|
170
171
|
this.identityRefreshUsed = false;
|
|
171
172
|
}
|
|
172
173
|
this.options.onMessage(event.data, frame);
|
|
173
|
-
};
|
|
174
|
+
});
|
|
174
175
|
socket.onerror = () => undefined;
|
|
175
|
-
socket.onclose = (event) => {
|
|
176
|
+
socket.onclose = contain('SessionSocket.close', (event: { code?: number }) => {
|
|
176
177
|
if (this.epoch === epoch) {
|
|
177
178
|
this.handleClose(epoch, event.code ?? STALE_CLOSE_CODE);
|
|
178
179
|
}
|
|
179
|
-
};
|
|
180
|
+
});
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
private handleClose(epoch: number, code: number): void {
|
|
@@ -210,17 +211,20 @@ export class SessionSocket {
|
|
|
210
211
|
if (isExhausted(SOCKET_BACKOFF, this.attempts)) {
|
|
211
212
|
this.options.onState('failed');
|
|
212
213
|
this.exhausted = true;
|
|
213
|
-
this.reconnectTimer = setTimeout(() => this.recover(), EXHAUSTED_RETRY_MS);
|
|
214
|
+
this.reconnectTimer = setTimeout(contain('SessionSocket.recover', () => this.recover()), EXHAUSTED_RETRY_MS);
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
const delay = backoffDelay(SOCKET_BACKOFF, this.attempts);
|
|
217
218
|
this.options.onState('reconnecting');
|
|
218
|
-
this.reconnectTimer = setTimeout(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
this.reconnectTimer = setTimeout(
|
|
220
|
+
contain('SessionSocket.reconnect', () => {
|
|
221
|
+
this.attempts += 1;
|
|
222
|
+
if (this.sessionId !== null) {
|
|
223
|
+
this.connect(this.sessionId);
|
|
224
|
+
}
|
|
225
|
+
}),
|
|
226
|
+
delay,
|
|
227
|
+
);
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
private recover(): void {
|
|
@@ -240,11 +244,14 @@ export class SessionSocket {
|
|
|
240
244
|
private startLiveness(): void {
|
|
241
245
|
this.stopLiveness();
|
|
242
246
|
this.lastFrameAt = Date.now();
|
|
243
|
-
this.livenessTimer = setInterval(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
this.livenessTimer = setInterval(
|
|
248
|
+
contain('SessionSocket.liveness', () => {
|
|
249
|
+
if (Date.now() - this.lastFrameAt >= STALE_AFTER_MS) {
|
|
250
|
+
this.dropStaleSocket();
|
|
251
|
+
}
|
|
252
|
+
}),
|
|
253
|
+
LIVENESS_CHECK_MS,
|
|
254
|
+
);
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
private stopLiveness(): void {
|