yaver-feedback-react-native 0.8.13 → 0.9.2
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/app.plugin.js +126 -9
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -2
- package/dist/FeedbackModal.js +477 -66
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +94 -1
- package/dist/P2PClient.js +281 -2
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +6 -1
- package/dist/VibeChatScreen.js +204 -1
- package/dist/YaverFeedback.d.ts +98 -0
- package/dist/YaverFeedback.js +509 -46
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
- package/dist/__tests__/ReportIdentity.test.js +168 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/device.d.ts +19 -9
- package/dist/_core/device.js +20 -14
- package/dist/capture.d.ts +6 -0
- package/dist/capture.js +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +215 -3
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +14 -3
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +563 -87
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +314 -2
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +219 -0
- package/src/YaverFeedback.ts +498 -46
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/ReportIdentity.test.ts +203 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/device.ts +20 -14
- package/src/capture.ts +51 -0
- package/src/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +217 -3
- package/src/voice.ts +270 -0
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlackBoxConfig } from './BlackBox';
|
|
1
2
|
/**
|
|
2
3
|
* Remote browser-style sign-in session for a coding-agent CLI on the
|
|
3
4
|
* connected yaver host. Mirrors runnerBrowserAuthSession on the agent
|
|
@@ -19,6 +20,53 @@ export interface RunnerBrowserAuthSession {
|
|
|
19
20
|
updatedAt: number;
|
|
20
21
|
completedAt?: number;
|
|
21
22
|
}
|
|
23
|
+
export interface RunnerAuthStatusRow {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
installed: boolean;
|
|
27
|
+
ready: boolean;
|
|
28
|
+
authConfigured: boolean;
|
|
29
|
+
authSource?: string;
|
|
30
|
+
warning?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface OpenCodeProviderSummary {
|
|
37
|
+
id: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
hasApiKey?: boolean;
|
|
40
|
+
baseUrl?: string;
|
|
41
|
+
models?: Array<{
|
|
42
|
+
id: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
provider?: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export interface OpenCodeAgentSummary {
|
|
48
|
+
name: string;
|
|
49
|
+
model?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
isBuiltin?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface OpenCodeConfigSummary {
|
|
54
|
+
path: string;
|
|
55
|
+
exists: boolean;
|
|
56
|
+
defaultAgent?: string;
|
|
57
|
+
model?: string;
|
|
58
|
+
smallModel?: string;
|
|
59
|
+
buildModel?: string;
|
|
60
|
+
planModel?: string;
|
|
61
|
+
providers?: OpenCodeProviderSummary[];
|
|
62
|
+
models?: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
provider?: string;
|
|
66
|
+
}>;
|
|
67
|
+
agents?: OpenCodeAgentSummary[];
|
|
68
|
+
diagnostics?: string[];
|
|
69
|
+
}
|
|
22
70
|
export interface IncidentEvent {
|
|
23
71
|
id: string;
|
|
24
72
|
timestamp: number;
|
|
@@ -74,9 +122,32 @@ export interface CapabilitySnapshot {
|
|
|
74
122
|
};
|
|
75
123
|
targets: Record<string, CapabilityTargetReadiness>;
|
|
76
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Opt-in config for the overlay's "App Store screenshots" action and the
|
|
127
|
+
* `capture_store_shots` remote command. When set, the SDK can walk the
|
|
128
|
+
* app's routes on-device, screenshot each, and upload them to the agent
|
|
129
|
+
* (which runs the App Store Connect backend).
|
|
130
|
+
*/
|
|
131
|
+
export interface StoreShotsConfig {
|
|
132
|
+
/** Ordered routes to visit + screenshot (e.g. ['/(tabs)/home', ...]). */
|
|
133
|
+
routes: string[];
|
|
134
|
+
/** Navigation handle: a react-navigation ref or expo-router `router`. */
|
|
135
|
+
navigationRef?: any;
|
|
136
|
+
/** Also set metadata + attempt submit-for-review after upload. */
|
|
137
|
+
submit?: boolean;
|
|
138
|
+
/** Optional per-route screenshot names (defaults to NN_<route>). */
|
|
139
|
+
screens?: string[];
|
|
140
|
+
}
|
|
77
141
|
export interface FeedbackConfig {
|
|
78
142
|
/** URL of the Yaver agent (e.g. "http://192.168.1.10:18080"). If omitted, auto-discovery is used. */
|
|
79
143
|
agentUrl?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Enables the overlay's "App Store screenshots" action + the
|
|
146
|
+
* `capture_store_shots` remote command. The host supplies the route
|
|
147
|
+
* list (and a navigation ref) once; the SDK captures the real running
|
|
148
|
+
* app — no simulator needed.
|
|
149
|
+
*/
|
|
150
|
+
storeShots?: StoreShotsConfig;
|
|
80
151
|
/**
|
|
81
152
|
* Auth token for the Yaver agent. Optional in 0.5+: if omitted, the SDK
|
|
82
153
|
* will hydrate one from AsyncStorage or show its in-app login screen
|
|
@@ -116,6 +187,39 @@ export interface FeedbackConfig {
|
|
|
116
187
|
* If omitted with convexUrl, connects to the first online device.
|
|
117
188
|
*/
|
|
118
189
|
preferredDeviceId?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Identity of the app this SDK instance reports for. Both fields are
|
|
192
|
+
* resolved automatically from expo-constants / native modules when
|
|
193
|
+
* omitted, so most apps never set them.
|
|
194
|
+
*
|
|
195
|
+
* Set them when the ambient answer would be wrong — above all when the
|
|
196
|
+
* app's JS is pushed into Yaver's container as a Hermes guest. There the
|
|
197
|
+
* runtime IS Yaver: `expo-constants` and `SettingsManager` describe the
|
|
198
|
+
* host (`Yaver` / `io.yaver.mobile`), not the guest, so auto-resolution
|
|
199
|
+
* would file every report against Yaver's own repo and the fix task would
|
|
200
|
+
* edit the SDK instead of the app. Declaring identity here is the only
|
|
201
|
+
* answer that holds in both a standalone build and a guest bundle.
|
|
202
|
+
*
|
|
203
|
+
* `bundleId` is the key that actually routes: the agent matches it against
|
|
204
|
+
* each project's pbxproj / build.gradle / app.json. `projectName` is a
|
|
205
|
+
* fallback for platforms with no bundle id.
|
|
206
|
+
*/
|
|
207
|
+
projectName?: string;
|
|
208
|
+
bundleId?: string;
|
|
209
|
+
/** Primary Yaver product surface for this app/package. */
|
|
210
|
+
surface?: YaverSurface;
|
|
211
|
+
/** All product surfaces this app/package owns. */
|
|
212
|
+
surfaces?: YaverSurface[];
|
|
213
|
+
/** Canonical development stack labels, e.g. react-native-expo, yaver-xml. */
|
|
214
|
+
stack?: string;
|
|
215
|
+
stacks?: string[];
|
|
216
|
+
/** Yaver preview/runtime targets that can exercise this app. */
|
|
217
|
+
testSurfaces?: string[];
|
|
218
|
+
feedbackSdk?: string;
|
|
219
|
+
feedbackTransport?: string;
|
|
220
|
+
voiceCapabilities?: string[];
|
|
221
|
+
sttProvider?: string;
|
|
222
|
+
ttsProvider?: string;
|
|
119
223
|
/** How feedback collection is triggered */
|
|
120
224
|
trigger?: 'shake' | 'floating-button' | 'manual';
|
|
121
225
|
/**
|
|
@@ -156,6 +260,22 @@ export interface FeedbackConfig {
|
|
|
156
260
|
* Calling `BlackBox.start()` manually is still safe — it's idempotent.
|
|
157
261
|
*/
|
|
158
262
|
autoStartBlackBox?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Config handed to BlackBox when the SDK auto-starts it.
|
|
265
|
+
*
|
|
266
|
+
* Without this there was no way to configure the flight recorder at all.
|
|
267
|
+
* The obvious move — and the one the README's own snippet makes — is:
|
|
268
|
+
*
|
|
269
|
+
* YaverFeedback.init({ trigger: 'shake' });
|
|
270
|
+
* BlackBox.start({ appName: 'my-app' });
|
|
271
|
+
*
|
|
272
|
+
* but that start() early-returns, because a zero-config init has no
|
|
273
|
+
* agentUrl yet (discovery resolves it asynchronously). The auto-start then
|
|
274
|
+
* fires ~500ms later once the agent and token exist and calls `start()`
|
|
275
|
+
* with NO arguments — so the host's config is silently dropped and appName
|
|
276
|
+
* is ''. Pass it here instead and the auto-start will honour it.
|
|
277
|
+
*/
|
|
278
|
+
blackBox?: BlackBoxConfig;
|
|
159
279
|
/**
|
|
160
280
|
* Small tap-to-open icon that floats above the app so the user
|
|
161
281
|
* doesn't have to shake every time they want to open feedback.
|
|
@@ -244,6 +364,26 @@ export interface FeedbackConfig {
|
|
|
244
364
|
* Bugsnag, or any other error tracking tool.
|
|
245
365
|
*/
|
|
246
366
|
maxCapturedErrors?: number;
|
|
367
|
+
/**
|
|
368
|
+
* Crash-aware reporting/fixing. Defaults to disabled.
|
|
369
|
+
*
|
|
370
|
+
* - enabled: records fatal errors into the SDK error buffer and enables
|
|
371
|
+
* explicit `YaverFeedback.reportCrash(error)`.
|
|
372
|
+
* - installGlobalHandler: opt-in only. Wraps React Native's global ErrorUtils
|
|
373
|
+
* handler without suppressing the existing handler. Leave false when Sentry,
|
|
374
|
+
* Crashlytics, Bugsnag, or another crash tool owns the global handler.
|
|
375
|
+
* - autoFix: after uploading a crash report, ask the connected Yaver agent to
|
|
376
|
+
* create a fix task. The normal BlackBox command stream can then deliver a
|
|
377
|
+
* reload/reload_bundle command after the agent rebuilds.
|
|
378
|
+
* - captureScreenshot: attach a best-effort screenshot to the crash report.
|
|
379
|
+
*/
|
|
380
|
+
crashReporting?: {
|
|
381
|
+
enabled?: boolean;
|
|
382
|
+
installGlobalHandler?: boolean;
|
|
383
|
+
autoFix?: boolean;
|
|
384
|
+
captureScreenshot?: boolean;
|
|
385
|
+
metadata?: Record<string, unknown>;
|
|
386
|
+
};
|
|
247
387
|
/**
|
|
248
388
|
* Which platforms the Build button targets.
|
|
249
389
|
* - 'ios' — build iOS only
|
|
@@ -356,9 +496,36 @@ export interface CapturedError {
|
|
|
356
496
|
}
|
|
357
497
|
export interface FeedbackMetadata {
|
|
358
498
|
timestamp: string;
|
|
359
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Wire key is `deviceInfo`, matching the Flutter + web SDKs and the
|
|
501
|
+
* agent's `FeedbackReport.DeviceInfo` (`json:"deviceInfo"`). This was
|
|
502
|
+
* `device` until 0.9.2 — a key the agent never read, so the whole block
|
|
503
|
+
* (platform, model, and the app name the fix router needs) was silently
|
|
504
|
+
* dropped on every React Native report. The agent still accepts the old
|
|
505
|
+
* key so builds already in the field keep working.
|
|
506
|
+
*/
|
|
507
|
+
deviceInfo: DeviceInfo;
|
|
360
508
|
app: AppInfo;
|
|
509
|
+
/**
|
|
510
|
+
* Which of the host's projects this report is about. The agent resolves
|
|
511
|
+
* a feedback→fix task's working directory from this; without it the task
|
|
512
|
+
* falls back to whatever directory the agent happens to be sitting in.
|
|
513
|
+
* Populated automatically by `resolveReportIdentity()`.
|
|
514
|
+
*/
|
|
515
|
+
project?: FeedbackProjectRef;
|
|
361
516
|
userNote?: string;
|
|
517
|
+
reportKind?: 'feedback' | 'auto-report' | 'crash';
|
|
518
|
+
crash?: {
|
|
519
|
+
message: string;
|
|
520
|
+
isFatal: boolean;
|
|
521
|
+
source: 'manual' | 'global-handler';
|
|
522
|
+
autoFixRequested?: boolean;
|
|
523
|
+
};
|
|
524
|
+
twin?: {
|
|
525
|
+
sessionId?: string;
|
|
526
|
+
surface?: string;
|
|
527
|
+
artifactDir?: string;
|
|
528
|
+
};
|
|
362
529
|
}
|
|
363
530
|
export interface DeviceInfo {
|
|
364
531
|
platform: string;
|
|
@@ -366,12 +533,49 @@ export interface DeviceInfo {
|
|
|
366
533
|
model: string;
|
|
367
534
|
screenWidth: number;
|
|
368
535
|
screenHeight: number;
|
|
536
|
+
/** Host app name. First key the agent's fix router checks. */
|
|
537
|
+
appName?: string;
|
|
369
538
|
}
|
|
370
539
|
export interface AppInfo {
|
|
371
540
|
bundleId?: string;
|
|
372
541
|
version?: string;
|
|
373
542
|
buildNumber?: string;
|
|
374
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Identifies the host project a report belongs to. Mirrors the web SDK's
|
|
546
|
+
* `FeedbackProjectRef` and the agent's `FeedbackProject`.
|
|
547
|
+
*/
|
|
548
|
+
export interface FeedbackProjectRef {
|
|
549
|
+
appName?: string;
|
|
550
|
+
projectName?: string;
|
|
551
|
+
/**
|
|
552
|
+
* Bundle identifier / applicationId. The only unambiguous key: an app's
|
|
553
|
+
* display name ("Talos") does not match the agent's registry name for it
|
|
554
|
+
* ("talos / mobile"), and one repo can hold several mobile projects that
|
|
555
|
+
* all share a name prefix. The agent matches this against the project's
|
|
556
|
+
* pbxproj, build.gradle, and app.json.
|
|
557
|
+
*/
|
|
558
|
+
bundleId?: string;
|
|
559
|
+
/**
|
|
560
|
+
* Deliberately never set by this SDK. The agent ignores client-supplied
|
|
561
|
+
* paths for feedback reports (an untrusted guest could otherwise point the
|
|
562
|
+
* fix task's CWD at ~/.ssh) and resolves the path server-side. Present only
|
|
563
|
+
* so the type matches the wire format the agent parses.
|
|
564
|
+
*/
|
|
565
|
+
projectPath?: string;
|
|
566
|
+
surface?: YaverSurface;
|
|
567
|
+
surfaces?: YaverSurface[];
|
|
568
|
+
stack?: string;
|
|
569
|
+
stacks?: string[];
|
|
570
|
+
testSurfaces?: string[];
|
|
571
|
+
feedbackSdk?: string;
|
|
572
|
+
feedbackTransport?: string;
|
|
573
|
+
voiceCapabilities?: string[];
|
|
574
|
+
sttProvider?: string;
|
|
575
|
+
ttsProvider?: string;
|
|
576
|
+
releaseChannel?: 'production' | 'candidate' | 'development';
|
|
577
|
+
}
|
|
578
|
+
export type YaverSurface = 'web' | 'mobile' | 'backend' | 'watch' | 'tv' | 'car' | 'vision' | 'desktop' | 'cli';
|
|
375
579
|
export interface TimelineEvent {
|
|
376
580
|
type: 'screenshot' | 'audio' | 'video';
|
|
377
581
|
path: string;
|
|
@@ -438,12 +642,20 @@ export interface TestSession {
|
|
|
438
642
|
export interface VoiceCapability {
|
|
439
643
|
/** Always true — mobile can always record and send audio. */
|
|
440
644
|
voiceInputEnabled: boolean;
|
|
441
|
-
/** Speech-to-speech provider (
|
|
645
|
+
/** Speech-to-speech provider (legacy), or null. */
|
|
442
646
|
s2sProvider?: string;
|
|
443
647
|
/** Whether the S2S provider is ready for real-time sessions. */
|
|
444
648
|
s2sReady?: boolean;
|
|
445
|
-
/** Speech-to-text provider for transcription
|
|
649
|
+
/** Speech-to-text provider for transcription, e.g. "deepgram" for Deepgram Flux. */
|
|
446
650
|
sttProvider?: string;
|
|
447
651
|
/** Whether STT is ready (auto-transcription of voice input). */
|
|
448
652
|
sttReady?: boolean;
|
|
653
|
+
/** Text-to-speech provider for readback, e.g. "cartesia". */
|
|
654
|
+
ttsProvider?: string;
|
|
655
|
+
/** Whether TTS readback is ready. */
|
|
656
|
+
ttsReady?: boolean;
|
|
657
|
+
/** Whether the agent-side hands-free task loop is enabled. */
|
|
658
|
+
enabled?: boolean;
|
|
659
|
+
/** Default project slug used by the agent voice loop. */
|
|
660
|
+
defaultProject?: string;
|
|
449
661
|
}
|
package/dist/voice.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface SDKVoiceStartOpts {
|
|
2
|
+
/** WS URL for the agent voice stream — P2PClient.voiceStreamUrl(). */
|
|
3
|
+
wsUrl: string;
|
|
4
|
+
/** Auth headers — P2PClient.voiceAuthHeaders(). */
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
project?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
runner?: string;
|
|
9
|
+
/** Surface hint for the agent's prompt wrapper. */
|
|
10
|
+
surface?: string;
|
|
11
|
+
/** Max chars for the spoken readback (Cartesia default ~280). */
|
|
12
|
+
ttsBudget?: number;
|
|
13
|
+
/** Per-session STT engine. "" = agent default. "local" = free
|
|
14
|
+
* whisper.cpp on the host; "deepgram" = Flux nova-3 streaming. */
|
|
15
|
+
sttProvider?: string;
|
|
16
|
+
/** Per-session TTS engine. "" = agent default. "local"/"device" =
|
|
17
|
+
* client synthesizes from the result text; cloud engines stream PCM. */
|
|
18
|
+
ttsProvider?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SDKVoiceCallbacks {
|
|
21
|
+
/** Active engines, echoed by the agent right after start — lets the UI
|
|
22
|
+
* show "Local" vs "Flux". */
|
|
23
|
+
onProviders?: (stt: string, tts: string) => void;
|
|
24
|
+
onTranscriptPartial?: (text: string) => void;
|
|
25
|
+
onTranscriptFinal?: (text: string) => void;
|
|
26
|
+
onTaskCreated?: (taskId: string) => void;
|
|
27
|
+
onTaskResult?: (taskId: string, text: string, status: string) => void;
|
|
28
|
+
onTTSReady?: (pcm: Uint8Array, sampleRate: number) => void;
|
|
29
|
+
onError?: (msg: string) => void;
|
|
30
|
+
onDone?: () => void;
|
|
31
|
+
}
|
|
32
|
+
/** True when the deps the voice stream needs (expo-file-system + buffer)
|
|
33
|
+
* are installed. The feedback UI hides the mic button otherwise. */
|
|
34
|
+
export declare function isVoiceStreamSupported(): boolean;
|
|
35
|
+
export declare class SDKVoiceSession {
|
|
36
|
+
private ws;
|
|
37
|
+
private callbacks;
|
|
38
|
+
private ttsChunks;
|
|
39
|
+
private ttsTotalBytes;
|
|
40
|
+
private ttsSampleRate;
|
|
41
|
+
private closed;
|
|
42
|
+
constructor(callbacks: SDKVoiceCallbacks);
|
|
43
|
+
/** Open the WS and send the start frame. Resolves once open. */
|
|
44
|
+
start(opts: SDKVoiceStartOpts): Promise<void>;
|
|
45
|
+
/** Stream a recorded WAV/PCM file as binary frames. Records produced by
|
|
46
|
+
* recordPcmWav() are LPCM 16-bit/16kHz mono with a 44-byte RIFF header
|
|
47
|
+
* we strip here — the exact shape the backend → STT expects. */
|
|
48
|
+
streamAudioFile(uri: string, opts?: {
|
|
49
|
+
skipWavHeader?: boolean;
|
|
50
|
+
chunkBytes?: number;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/** Done speaking — flush STT and create the agent task. */
|
|
53
|
+
finalize(): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
private handleMessage;
|
|
56
|
+
}
|
|
57
|
+
/** Wrap raw PCM (signed 16-bit LE) in a minimal WAV container for
|
|
58
|
+
* expo-av playback. 44-byte header + samples. */
|
|
59
|
+
export declare function wrapPCMAsWAV(pcm: Uint8Array, sampleRate: number, channels?: number): Uint8Array;
|
|
60
|
+
/** Write a PCM buffer to a temp WAV file; returns its file:// URI. */
|
|
61
|
+
export declare function pcmToTempWavURI(pcm: Uint8Array, sampleRate: number): Promise<string>;
|
package/dist/voice.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Voice vibe-coding for the feedback SDK — speak a change, the agent
|
|
3
|
+
// acts (full MCP), and reads back a spoken headline. Ported from the
|
|
4
|
+
// Yaver mobile app's AgentVoiceSession (mobile/src/lib/agentVoice.ts) but
|
|
5
|
+
// decoupled from the app: connection details (WS URL + auth headers) come
|
|
6
|
+
// from the SDK's P2PClient, so it works both inside the Yaver container
|
|
7
|
+
// (inherited auth) and standalone in a third-party app.
|
|
8
|
+
//
|
|
9
|
+
// Local vs remote STT/TTS is decided entirely agent-side (free local
|
|
10
|
+
// whisper.cpp by default, Deepgram/OpenAI/etc when keys are configured),
|
|
11
|
+
// so nothing model-related is bundled here — the SDK just streams mic
|
|
12
|
+
// audio and renders the transcript + TTS the agent sends back.
|
|
13
|
+
//
|
|
14
|
+
// expo-file-system + buffer are loaded lazily so a bare-RN app that
|
|
15
|
+
// hasn't installed them can still use the rest of the SDK; the voice
|
|
16
|
+
// button hides itself via isVoiceStreamSupported() when they're absent.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.SDKVoiceSession = void 0;
|
|
19
|
+
exports.isVoiceStreamSupported = isVoiceStreamSupported;
|
|
20
|
+
exports.wrapPCMAsWAV = wrapPCMAsWAV;
|
|
21
|
+
exports.pcmToTempWavURI = pcmToTempWavURI;
|
|
22
|
+
function loadBuffer() {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
24
|
+
return require('buffer').Buffer;
|
|
25
|
+
}
|
|
26
|
+
function loadFS() {
|
|
27
|
+
// expo-file-system v16+ moved the classic API under /legacy; fall back
|
|
28
|
+
// to the root export for older installs.
|
|
29
|
+
try {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
31
|
+
return require('expo-file-system/legacy');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
35
|
+
return require('expo-file-system');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** True when the deps the voice stream needs (expo-file-system + buffer)
|
|
39
|
+
* are installed. The feedback UI hides the mic button otherwise. */
|
|
40
|
+
function isVoiceStreamSupported() {
|
|
41
|
+
try {
|
|
42
|
+
loadBuffer();
|
|
43
|
+
const fs = loadFS();
|
|
44
|
+
return typeof fs?.readAsStringAsync === 'function';
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class SDKVoiceSession {
|
|
51
|
+
constructor(callbacks) {
|
|
52
|
+
this.ws = null;
|
|
53
|
+
this.ttsChunks = [];
|
|
54
|
+
this.ttsTotalBytes = 0;
|
|
55
|
+
this.ttsSampleRate = 22050;
|
|
56
|
+
this.closed = false;
|
|
57
|
+
this.callbacks = callbacks;
|
|
58
|
+
}
|
|
59
|
+
/** Open the WS and send the start frame. Resolves once open. */
|
|
60
|
+
async start(opts) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
let opened = false;
|
|
63
|
+
let ws;
|
|
64
|
+
try {
|
|
65
|
+
// RN-specific 3rd arg carries request headers.
|
|
66
|
+
ws = new WebSocket(opts.wsUrl, undefined, { headers: opts.headers });
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
reject(e);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.ws = ws;
|
|
73
|
+
ws.onopen = () => {
|
|
74
|
+
opened = true;
|
|
75
|
+
try {
|
|
76
|
+
ws.send(JSON.stringify({
|
|
77
|
+
type: 'start',
|
|
78
|
+
project: opts.project ?? '',
|
|
79
|
+
model: opts.model ?? '',
|
|
80
|
+
runner: opts.runner ?? '',
|
|
81
|
+
surface: opts.surface ?? '',
|
|
82
|
+
ttsBudget: opts.ttsBudget ?? 0,
|
|
83
|
+
sttProvider: opts.sttProvider ?? '',
|
|
84
|
+
ttsProvider: opts.ttsProvider ?? '',
|
|
85
|
+
}));
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
reject(e);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
ws.onmessage = (e) => this.handleMessage(e);
|
|
93
|
+
ws.onerror = () => {
|
|
94
|
+
if (!opened)
|
|
95
|
+
reject(new Error('voice WS connect failed'));
|
|
96
|
+
else
|
|
97
|
+
this.callbacks.onError?.('voice WS error');
|
|
98
|
+
};
|
|
99
|
+
ws.onclose = () => {
|
|
100
|
+
this.closed = true;
|
|
101
|
+
if (!opened)
|
|
102
|
+
reject(new Error('voice WS closed before open'));
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Stream a recorded WAV/PCM file as binary frames. Records produced by
|
|
107
|
+
* recordPcmWav() are LPCM 16-bit/16kHz mono with a 44-byte RIFF header
|
|
108
|
+
* we strip here — the exact shape the backend → STT expects. */
|
|
109
|
+
async streamAudioFile(uri, opts) {
|
|
110
|
+
if (!this.ws || this.closed)
|
|
111
|
+
throw new Error('voice WS not open');
|
|
112
|
+
const Buffer = loadBuffer();
|
|
113
|
+
const FileSystem = loadFS();
|
|
114
|
+
const skipWavHeader = opts?.skipWavHeader ?? true;
|
|
115
|
+
const chunkBytes = opts?.chunkBytes ?? 16384;
|
|
116
|
+
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: 'base64' });
|
|
117
|
+
let buf = Buffer.from(b64, 'base64');
|
|
118
|
+
if (skipWavHeader && buf.length > 44 && buf.slice(0, 4).toString() === 'RIFF') {
|
|
119
|
+
buf = buf.slice(44);
|
|
120
|
+
}
|
|
121
|
+
for (let i = 0; i < buf.length; i += chunkBytes) {
|
|
122
|
+
if (this.closed)
|
|
123
|
+
return;
|
|
124
|
+
const slice = buf.slice(i, Math.min(i + chunkBytes, buf.length));
|
|
125
|
+
this.ws.send(slice.buffer.slice(slice.byteOffset, slice.byteOffset + slice.byteLength));
|
|
126
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Done speaking — flush STT and create the agent task. */
|
|
130
|
+
finalize() {
|
|
131
|
+
if (this.ws && !this.closed) {
|
|
132
|
+
this.ws.send(JSON.stringify({ type: 'stop' }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
close() {
|
|
136
|
+
this.closed = true;
|
|
137
|
+
try {
|
|
138
|
+
this.ws?.close();
|
|
139
|
+
}
|
|
140
|
+
catch { /* ignore */ }
|
|
141
|
+
this.ws = null;
|
|
142
|
+
}
|
|
143
|
+
handleMessage(e) {
|
|
144
|
+
let msg;
|
|
145
|
+
try {
|
|
146
|
+
msg = JSON.parse(typeof e.data === 'string' ? e.data : '');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
switch (msg.type) {
|
|
152
|
+
case 'providers':
|
|
153
|
+
this.callbacks.onProviders?.(msg.stt ?? '', msg.tts ?? '');
|
|
154
|
+
break;
|
|
155
|
+
case 'transcript-partial':
|
|
156
|
+
this.callbacks.onTranscriptPartial?.(msg.text ?? '');
|
|
157
|
+
break;
|
|
158
|
+
case 'transcript-final':
|
|
159
|
+
this.callbacks.onTranscriptFinal?.(msg.text ?? '');
|
|
160
|
+
break;
|
|
161
|
+
case 'task-created':
|
|
162
|
+
this.callbacks.onTaskCreated?.(msg.taskId ?? '');
|
|
163
|
+
break;
|
|
164
|
+
case 'task-result':
|
|
165
|
+
this.callbacks.onTaskResult?.(msg.taskId ?? '', msg.text ?? '', msg.status ?? '');
|
|
166
|
+
break;
|
|
167
|
+
case 'tts-frame': {
|
|
168
|
+
if (!msg.pcm)
|
|
169
|
+
break;
|
|
170
|
+
const Buffer = loadBuffer();
|
|
171
|
+
const arr = new Uint8Array(Buffer.from(msg.pcm, 'base64'));
|
|
172
|
+
this.ttsChunks.push(arr);
|
|
173
|
+
this.ttsTotalBytes += arr.length;
|
|
174
|
+
if (msg.sampleRate)
|
|
175
|
+
this.ttsSampleRate = msg.sampleRate;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'done': {
|
|
179
|
+
if (this.ttsTotalBytes > 0) {
|
|
180
|
+
const pcm = new Uint8Array(this.ttsTotalBytes);
|
|
181
|
+
let off = 0;
|
|
182
|
+
for (const c of this.ttsChunks) {
|
|
183
|
+
pcm.set(c, off);
|
|
184
|
+
off += c.length;
|
|
185
|
+
}
|
|
186
|
+
this.callbacks.onTTSReady?.(pcm, this.ttsSampleRate);
|
|
187
|
+
}
|
|
188
|
+
this.callbacks.onDone?.();
|
|
189
|
+
this.close();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'error':
|
|
193
|
+
this.callbacks.onError?.(msg.error ?? 'voice error');
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.SDKVoiceSession = SDKVoiceSession;
|
|
199
|
+
/** Wrap raw PCM (signed 16-bit LE) in a minimal WAV container for
|
|
200
|
+
* expo-av playback. 44-byte header + samples. */
|
|
201
|
+
function wrapPCMAsWAV(pcm, sampleRate, channels = 1) {
|
|
202
|
+
const bitsPerSample = 16;
|
|
203
|
+
const byteRate = (sampleRate * channels * bitsPerSample) / 8;
|
|
204
|
+
const blockAlign = (channels * bitsPerSample) / 8;
|
|
205
|
+
const dataSize = pcm.length;
|
|
206
|
+
const buf = new Uint8Array(44 + dataSize);
|
|
207
|
+
const dv = new DataView(buf.buffer);
|
|
208
|
+
buf[0] = 0x52;
|
|
209
|
+
buf[1] = 0x49;
|
|
210
|
+
buf[2] = 0x46;
|
|
211
|
+
buf[3] = 0x46; // RIFF
|
|
212
|
+
dv.setUint32(4, 36 + dataSize, true);
|
|
213
|
+
buf[8] = 0x57;
|
|
214
|
+
buf[9] = 0x41;
|
|
215
|
+
buf[10] = 0x56;
|
|
216
|
+
buf[11] = 0x45; // WAVE
|
|
217
|
+
buf[12] = 0x66;
|
|
218
|
+
buf[13] = 0x6d;
|
|
219
|
+
buf[14] = 0x74;
|
|
220
|
+
buf[15] = 0x20; // "fmt "
|
|
221
|
+
dv.setUint32(16, 16, true);
|
|
222
|
+
dv.setUint16(20, 1, true);
|
|
223
|
+
dv.setUint16(22, channels, true);
|
|
224
|
+
dv.setUint32(24, sampleRate, true);
|
|
225
|
+
dv.setUint32(28, byteRate, true);
|
|
226
|
+
dv.setUint16(32, blockAlign, true);
|
|
227
|
+
dv.setUint16(34, bitsPerSample, true);
|
|
228
|
+
buf[36] = 0x64;
|
|
229
|
+
buf[37] = 0x61;
|
|
230
|
+
buf[38] = 0x74;
|
|
231
|
+
buf[39] = 0x61; // data
|
|
232
|
+
dv.setUint32(40, dataSize, true);
|
|
233
|
+
buf.set(pcm, 44);
|
|
234
|
+
return buf;
|
|
235
|
+
}
|
|
236
|
+
/** Write a PCM buffer to a temp WAV file; returns its file:// URI. */
|
|
237
|
+
async function pcmToTempWavURI(pcm, sampleRate) {
|
|
238
|
+
const Buffer = loadBuffer();
|
|
239
|
+
const FileSystem = loadFS();
|
|
240
|
+
const wav = wrapPCMAsWAV(pcm, sampleRate);
|
|
241
|
+
const b64 = Buffer.from(wav).toString('base64');
|
|
242
|
+
const dir = FileSystem.cacheDirectory ?? FileSystem.documentDirectory ?? '';
|
|
243
|
+
const path = `${dir}yaver-voice-tts-${Date.now()}.wav`;
|
|
244
|
+
await FileSystem.writeAsStringAsync(path, b64, { encoding: 'base64' });
|
|
245
|
+
return path;
|
|
246
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Visual feedback SDK for Yaver
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice vibe coding, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -26,9 +26,20 @@
|
|
|
26
26
|
"expo-document-picker": ">=13.0.0",
|
|
27
27
|
"react-native-view-shot": ">=3.0.0",
|
|
28
28
|
"react-native-record-screen": ">=0.0.10",
|
|
29
|
-
"expo-av": ">=14.0.0"
|
|
29
|
+
"expo-av": ">=14.0.0",
|
|
30
|
+
"expo-file-system": ">=16.0.0",
|
|
31
|
+
"expo-speech": ">=12.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"buffer": "^6.0.3"
|
|
30
35
|
},
|
|
31
36
|
"peerDependenciesMeta": {
|
|
37
|
+
"expo-file-system": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"expo-speech": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
32
43
|
"@expo/config-plugins": {
|
|
33
44
|
"optional": true
|
|
34
45
|
},
|
package/src/BlackBox.ts
CHANGED
|
@@ -66,6 +66,13 @@ export interface BlackBoxCommand {
|
|
|
66
66
|
/** Callback type for handling agent commands. */
|
|
67
67
|
export type CommandHandler = (cmd: BlackBoxCommand) => void;
|
|
68
68
|
|
|
69
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>): void {
|
|
70
|
+
const maybeNodeTimer = timer as unknown as { unref?: () => void };
|
|
71
|
+
if (typeof maybeNodeTimer.unref === 'function') {
|
|
72
|
+
maybeNodeTimer.unref();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export class BlackBox {
|
|
70
77
|
private static baseUrl: string | null = null;
|
|
71
78
|
private static authToken: string | null = null;
|
|
@@ -109,7 +116,7 @@ export class BlackBox {
|
|
|
109
116
|
|
|
110
117
|
BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
|
|
111
118
|
BlackBox.authToken = feedbackConfig.authToken ?? null;
|
|
112
|
-
BlackBox.relayPassword = (
|
|
119
|
+
BlackBox.relayPassword = YaverFeedback.getRelayPassword();
|
|
113
120
|
BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
|
|
114
121
|
BlackBox.appName = config?.appName ?? '';
|
|
115
122
|
BlackBox.flushInterval = config?.flushInterval ?? 2000;
|
|
@@ -120,6 +127,7 @@ export class BlackBox {
|
|
|
120
127
|
// Start periodic flush
|
|
121
128
|
if (BlackBox.flushTimer) clearInterval(BlackBox.flushTimer);
|
|
122
129
|
BlackBox.flushTimer = setInterval(() => BlackBox.flush(), BlackBox.flushInterval);
|
|
130
|
+
unrefTimer(BlackBox.flushTimer);
|
|
123
131
|
|
|
124
132
|
// Log the session start
|
|
125
133
|
BlackBox.push({
|
|
@@ -471,6 +479,7 @@ export class BlackBox {
|
|
|
471
479
|
BlackBox.sseReconnectTimer = null;
|
|
472
480
|
if (BlackBox.started) BlackBox.connectSSE();
|
|
473
481
|
}, 5000);
|
|
482
|
+
unrefTimer(BlackBox.sseReconnectTimer);
|
|
474
483
|
}
|
|
475
484
|
|
|
476
485
|
// ─── Internal ────────────────────────────────────────────────────
|