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.
Files changed (76) hide show
  1. package/app.plugin.js +126 -9
  2. package/dist/BlackBox.js +9 -1
  3. package/dist/DeployPanel.js +65 -0
  4. package/dist/Discovery.js +13 -2
  5. package/dist/FeedbackModal.js +477 -66
  6. package/dist/MachinePickerScreen.js +14 -5
  7. package/dist/P2PClient.d.ts +94 -1
  8. package/dist/P2PClient.js +281 -2
  9. package/dist/ShakeDetector.js +2 -0
  10. package/dist/VibeChatScreen.d.ts +6 -1
  11. package/dist/VibeChatScreen.js +204 -1
  12. package/dist/YaverFeedback.d.ts +98 -0
  13. package/dist/YaverFeedback.js +509 -46
  14. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  15. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  16. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  17. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  18. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  19. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  20. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  21. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  22. package/dist/__tests__/P2PClient.test.js +2 -2
  23. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  24. package/dist/__tests__/ReportIdentity.test.js +168 -0
  25. package/dist/__tests__/SDKToken.test.js +1 -1
  26. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  27. package/dist/__tests__/ShakeToggle.test.js +121 -0
  28. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  29. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  30. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  31. package/dist/__tests__/reloadActions.test.js +129 -0
  32. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  33. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  34. package/dist/__tests__/types.test.js +5 -5
  35. package/dist/_core/device.d.ts +19 -9
  36. package/dist/_core/device.js +20 -14
  37. package/dist/capture.d.ts +6 -0
  38. package/dist/capture.js +53 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +11 -1
  41. package/dist/reloadActions.d.ts +88 -0
  42. package/dist/reloadActions.js +200 -0
  43. package/dist/storeShots.d.ts +67 -0
  44. package/dist/storeShots.js +137 -0
  45. package/dist/types.d.ts +215 -3
  46. package/dist/voice.d.ts +61 -0
  47. package/dist/voice.js +246 -0
  48. package/package.json +14 -3
  49. package/src/BlackBox.ts +10 -1
  50. package/src/DeployPanel.tsx +74 -0
  51. package/src/Discovery.ts +13 -2
  52. package/src/FeedbackModal.tsx +563 -87
  53. package/src/MachinePickerScreen.tsx +12 -3
  54. package/src/P2PClient.ts +314 -2
  55. package/src/ShakeDetector.ts +1 -0
  56. package/src/VibeChatScreen.tsx +219 -0
  57. package/src/YaverFeedback.ts +498 -46
  58. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  59. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  60. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  61. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  62. package/src/__tests__/P2PClient.test.ts +2 -2
  63. package/src/__tests__/ReportIdentity.test.ts +203 -0
  64. package/src/__tests__/SDKToken.test.ts +1 -1
  65. package/src/__tests__/ShakeToggle.test.ts +153 -0
  66. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  67. package/src/__tests__/reloadActions.test.ts +171 -0
  68. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  69. package/src/__tests__/types.test.ts +5 -5
  70. package/src/_core/device.ts +20 -14
  71. package/src/capture.ts +51 -0
  72. package/src/index.ts +21 -0
  73. package/src/reloadActions.ts +273 -0
  74. package/src/storeShots.ts +189 -0
  75. package/src/types.ts +217 -3
  76. package/src/voice.ts +270 -0
@@ -104,7 +104,14 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
104
104
  return;
105
105
  }
106
106
  const direct = await (0, auth_1.probeDeviceReachability)(device);
107
- if (!direct.reachable && !device.needsAuth) {
107
+ // Do not hard-block selection just because the LAN /health probe
108
+ // failed. The standalone SDK can still reach a healthy machine via
109
+ // the normal selected-device discovery path (including relay), and
110
+ // the Yaver host path may already be proving the machine works.
111
+ // Only treat the machine as unpickable when BOTH:
112
+ // 1. Convex says it is offline, and
113
+ // 2. the direct probe also failed.
114
+ if (!device.isOnline && !direct.reachable && !device.needsAuth) {
108
115
  setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
109
116
  setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
110
117
  return;
@@ -131,9 +138,11 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
131
138
  ? '#f59e0b'
132
139
  : effectivelyReachable
133
140
  ? '#22c55e'
134
- : explicitlyOffline || !device.isOnline
135
- ? '#ef4444'
136
- : '#22c55e';
141
+ : device.isOnline
142
+ ? '#f59e0b'
143
+ : explicitlyOffline || !device.isOnline
144
+ ? '#ef4444'
145
+ : '#22c55e';
137
146
  // Derive a single short status phrase the user can act on.
138
147
  let statusLine = device.platform;
139
148
  if (probe === undefined) {
@@ -150,7 +159,7 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
150
159
  'Needs pairing — open the Yaver app to adopt this machine';
151
160
  }
152
161
  else if (explicitlyOffline) {
153
- statusLine = 'Agent not responding on this machine';
162
+ statusLine = 'Online, but direct probe failed — relay / selected-machine path may still work';
154
163
  }
155
164
  else if (device.runnerDown) {
156
165
  statusLine = 'Runner down — restart the coding agent on the Mac';
@@ -1,4 +1,5 @@
1
- import { CapabilitySnapshot, FeedbackBundle, IncidentEvent, OperationState, RunnerBrowserAuthSession, TestSession, VoiceCapability } from './types';
1
+ import { AppInfo, CapabilitySnapshot, FeedbackBundle, FeedbackProjectRef, IncidentEvent, OpenCodeConfigSummary, OperationState, RunnerBrowserAuthSession, RunnerAuthStatusRow, TestSession, VoiceCapability } from './types';
2
+ import type { DevServerSnapshot, ReloadWireMode } from './reloadActions';
2
3
  export interface FeedbackEvent {
3
4
  type: string;
4
5
  timestamp: string;
@@ -28,6 +29,45 @@ export declare function resolveAppIdentity(opts?: {
28
29
  bundleId?: string;
29
30
  projectPath?: string;
30
31
  };
32
+ /**
33
+ * Build the app-identity half of a feedback report's metadata.
34
+ *
35
+ * `resolveAppIdentity()` has always fed /vibing/execute and /dev/reload-app,
36
+ * so the agent could route a "vibe on THIS app" request to the right repo —
37
+ * but nothing fed /feedback. Reports arrived with no identity at all, so the
38
+ * agent's fix router fell through to its own working directory and edited
39
+ * whichever repo it happened to be sitting in. This closes that gap by
40
+ * reusing the same resolver for the feedback path.
41
+ *
42
+ * Precedence, most to least trustworthy:
43
+ * 1. What the host app declared (`FeedbackConfig.projectName`/`bundleId`).
44
+ * An app knows its own identity; nothing should override it.
45
+ * 2. The guest project Yaver's host shell pinned, when running as a Hermes
46
+ * guest. Ambient lookups describe Yaver there, not the guest.
47
+ * 3. Ambient expo-constants / native modules — correct for a standalone
48
+ * build, which is the common case.
49
+ *
50
+ * Every lookup is best-effort: a bare RN app with no expo-constants still
51
+ * produces a report, just one the agent resolves by its own means.
52
+ */
53
+ export declare function resolveReportIdentity(opts?: {
54
+ projectName?: string;
55
+ bundleId?: string;
56
+ surface?: FeedbackProjectRef['surface'];
57
+ surfaces?: FeedbackProjectRef['surfaces'];
58
+ stack?: string;
59
+ stacks?: string[];
60
+ testSurfaces?: string[];
61
+ feedbackSdk?: string;
62
+ feedbackTransport?: string;
63
+ voiceCapabilities?: string[];
64
+ sttProvider?: string;
65
+ ttsProvider?: string;
66
+ }): {
67
+ appName?: string;
68
+ app: AppInfo;
69
+ project?: FeedbackProjectRef;
70
+ };
31
71
  /**
32
72
  * Lightweight P2P HTTP client for communicating with a Yaver agent.
33
73
  *
@@ -51,6 +91,16 @@ export declare class P2PClient {
51
91
  setAuthToken(token: string): void;
52
92
  /** Update the relay password (used for managed-relay baseUrls). */
53
93
  setRelayPassword(password: string): void;
94
+ /** Read-only base URL — used by the voice vibe-coding path to probe
95
+ * GET /voice/status before opening the stream. */
96
+ get agentBaseUrl(): string;
97
+ /** WebSocket URL for the agent's voice stream (WS /voice/stream). The
98
+ * voice vibe-coding loop streams mic audio here and receives the
99
+ * transcript + agent task + TTS frames back. */
100
+ voiceStreamUrl(): string;
101
+ /** Auth headers for the voice WS + status probe — same bearer (and
102
+ * relay password) as every other agent request. */
103
+ voiceAuthHeaders(): Record<string, string>;
54
104
  /** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
55
105
  private authHeaders;
56
106
  /**
@@ -69,6 +119,26 @@ export declare class P2PClient {
69
119
  * the SDK still exposes it for symmetry with mobile/src/components/
70
120
  * RunnerAuthModal.tsx and the Swift YaverRunnerAuthFlowPane. */
71
121
  submitRunnerBrowserAuthCode(sessionId: string, code: string): Promise<RunnerBrowserAuthSession>;
122
+ getRunnerAuthStatus(): Promise<RunnerAuthStatusRow[]>;
123
+ getOpenCodeConfig(): Promise<OpenCodeConfigSummary | null>;
124
+ saveOpenCodeConfig(patch: {
125
+ defaultAgent?: string;
126
+ model?: string;
127
+ smallModel?: string;
128
+ buildModel?: string;
129
+ planModel?: string;
130
+ providers?: Array<{
131
+ id: string;
132
+ name?: string;
133
+ baseUrl?: string;
134
+ apiKey?: string;
135
+ delete?: boolean;
136
+ }>;
137
+ }): Promise<{
138
+ ok: boolean;
139
+ config?: OpenCodeConfigSummary;
140
+ error?: string;
141
+ }>;
72
142
  capabilitySnapshot(): Promise<CapabilitySnapshot | null>;
73
143
  incidents(opts?: {
74
144
  category?: string;
@@ -133,6 +203,29 @@ export declare class P2PClient {
133
203
  * via the BlackBox command channel.
134
204
  * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
135
205
  */
206
+ /**
207
+ * Read the dev server's state so the overlay can decide WHICH reload
208
+ * actions to offer, and disable the rest with a reason.
209
+ *
210
+ * Returns null when the machine cannot be reached at all — which the
211
+ * caller must render as "not connected", never as "no dev server".
212
+ * Those are two different problems with two different fixes.
213
+ */
214
+ getDevServerStatus(): Promise<DevServerSnapshot | null>;
215
+ /**
216
+ * Trigger a reload with an EXPLICIT fast/full mode — no bundle fallback.
217
+ *
218
+ * `reloadApp('dev')` silently falls through to a bundle rebuild when the
219
+ * dev server is down, which is right for a one-button UX and wrong the
220
+ * moment the user picks between two named actions: someone who pressed
221
+ * "Full Reload" must not get a 60-second bundle rebuild without being
222
+ * told. So this method reports the failure instead, with a named cause.
223
+ *
224
+ * Auth: the SAME bearer used for the feedback POST. `/dev/reload` is
225
+ * registered under `authSDKOrGuest` in desktop/agent/httpserver.go and is
226
+ * already in the `guest-reload` SDK-token scope list — nothing widens.
227
+ */
228
+ reloadWithMode(mode: ReloadWireMode, snapshot?: DevServerSnapshot | null): Promise<ReloadAck>;
136
229
  reloadApp(mode?: 'dev' | 'bundle', opts?: {
137
230
  projectName?: string;
138
231
  bundleId?: string;
package/dist/P2PClient.js CHANGED
@@ -35,7 +35,16 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.P2PClient = void 0;
37
37
  exports.resolveAppIdentity = resolveAppIdentity;
38
+ exports.resolveReportIdentity = resolveReportIdentity;
38
39
  const react_native_1 = require("react-native");
40
+ const endpoints_1 = require("./_core/endpoints");
41
+ const reloadActions_1 = require("./reloadActions");
42
+ function unrefTimer(timer) {
43
+ const maybeNodeTimer = timer;
44
+ if (typeof maybeNodeTimer.unref === 'function') {
45
+ maybeNodeTimer.unref();
46
+ }
47
+ }
39
48
  /**
40
49
  * Try to resolve `{projectName, bundleId}` for the running app so the
41
50
  * agent can map the reload request to a MobileProject in its scan
@@ -84,6 +93,140 @@ function resolveAppIdentity(opts) {
84
93
  out.projectPath = projectPath;
85
94
  return out;
86
95
  }
96
+ /**
97
+ * Is this JS running inside Yaver's own container as a pushed Hermes guest?
98
+ *
99
+ * The YaverInfo native module is registered only by Yaver's app, so its
100
+ * presence means the ambient runtime describes Yaver rather than the app
101
+ * whose code is executing. Mirrors the checks in YaverFeedback.ts,
102
+ * ShakeDetector.ts and QuickActionIcon.tsx.
103
+ */
104
+ function isInsideYaverContainer() {
105
+ try {
106
+ const { NativeModules } = require('react-native');
107
+ return !!NativeModules?.YaverInfo;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ /**
114
+ * Read the guest project Yaver's host shell pinned for this bundle.
115
+ *
116
+ * When an app's JS is pushed into Yaver's container as a Hermes guest, the
117
+ * ambient runtime describes the HOST — `expo-constants` and `SettingsManager`
118
+ * both answer `Yaver` / `io.yaver.mobile`. Auto-resolution would therefore
119
+ * file every guest report against Yaver's own repo, and the fix task would
120
+ * edit the SDK instead of the app under test.
121
+ *
122
+ * Yaver already knows the right answer: picking a project in the Hot Reload
123
+ * tab calls `YaverInfo.setInheritedGuestProject(name, path)`. Only the name
124
+ * is used here — the agent resolves the path itself and deliberately ignores
125
+ * client-supplied ones on feedback reports.
126
+ */
127
+ function resolveInheritedGuestProjectName() {
128
+ try {
129
+ const { NativeModules } = require('react-native');
130
+ const name = String(NativeModules?.YaverInfo?.inheritedGuestProjectName || '').trim();
131
+ return name || undefined;
132
+ }
133
+ catch {
134
+ // Not inside Yaver's container, or react-native unavailable (unit tests).
135
+ return undefined;
136
+ }
137
+ }
138
+ /**
139
+ * Build the app-identity half of a feedback report's metadata.
140
+ *
141
+ * `resolveAppIdentity()` has always fed /vibing/execute and /dev/reload-app,
142
+ * so the agent could route a "vibe on THIS app" request to the right repo —
143
+ * but nothing fed /feedback. Reports arrived with no identity at all, so the
144
+ * agent's fix router fell through to its own working directory and edited
145
+ * whichever repo it happened to be sitting in. This closes that gap by
146
+ * reusing the same resolver for the feedback path.
147
+ *
148
+ * Precedence, most to least trustworthy:
149
+ * 1. What the host app declared (`FeedbackConfig.projectName`/`bundleId`).
150
+ * An app knows its own identity; nothing should override it.
151
+ * 2. The guest project Yaver's host shell pinned, when running as a Hermes
152
+ * guest. Ambient lookups describe Yaver there, not the guest.
153
+ * 3. Ambient expo-constants / native modules — correct for a standalone
154
+ * build, which is the common case.
155
+ *
156
+ * Every lookup is best-effort: a bare RN app with no expo-constants still
157
+ * produces a report, just one the agent resolves by its own means.
158
+ */
159
+ function resolveReportIdentity(opts) {
160
+ let projectName = (opts?.projectName || '').trim() || undefined;
161
+ let bundleId = (opts?.bundleId || '').trim() || undefined;
162
+ let version;
163
+ let buildNumber;
164
+ if (isInsideYaverContainer()) {
165
+ // Every ambient lookup answers for the HOST here. Because the agent
166
+ // routes on bundle id first, letting Yaver's id through would resolve
167
+ // straight to Yaver's own repo and the guest's name would never be
168
+ // consulted — so nothing ambient is trusted in the container. The
169
+ // version is Yaver's too, and reporting it would only mislabel which
170
+ // build the report came from.
171
+ projectName = projectName || resolveInheritedGuestProjectName();
172
+ }
173
+ else {
174
+ const ambient = resolveAppIdentity({ projectName, bundleId });
175
+ projectName = ambient.projectName;
176
+ bundleId = ambient.bundleId;
177
+ try {
178
+ const Constants = require('expo-constants').default ?? require('expo-constants');
179
+ const cfg = Constants?.expoConfig ?? Constants?.manifest ?? {};
180
+ version = Constants?.nativeAppVersion || cfg?.version;
181
+ buildNumber =
182
+ Constants?.nativeBuildVersion ||
183
+ cfg?.ios?.buildNumber ||
184
+ (cfg?.android?.versionCode != null ? String(cfg.android.versionCode) : undefined);
185
+ }
186
+ catch {
187
+ // expo-constants not installed (bare RN). Version is cosmetic — the
188
+ // router keys off appName/bundleId, both of which survive without it.
189
+ }
190
+ }
191
+ const app = {};
192
+ if (bundleId)
193
+ app.bundleId = bundleId;
194
+ if (version)
195
+ app.version = version;
196
+ if (buildNumber)
197
+ app.buildNumber = buildNumber;
198
+ if (!projectName && !bundleId) {
199
+ return { app };
200
+ }
201
+ const project = { surface: opts?.surface ?? 'mobile' };
202
+ if (projectName) {
203
+ project.projectName = projectName;
204
+ project.appName = projectName;
205
+ }
206
+ // Note: projectPath is intentionally omitted — the agent ignores
207
+ // client-supplied paths on feedback reports and resolves them itself.
208
+ if (bundleId)
209
+ project.bundleId = bundleId;
210
+ if (opts?.surfaces)
211
+ project.surfaces = opts.surfaces;
212
+ if (opts?.stack)
213
+ project.stack = opts.stack;
214
+ if (opts?.stacks)
215
+ project.stacks = opts.stacks;
216
+ if (opts?.testSurfaces)
217
+ project.testSurfaces = opts.testSurfaces;
218
+ if (opts?.feedbackSdk)
219
+ project.feedbackSdk = opts.feedbackSdk;
220
+ if (opts?.feedbackTransport)
221
+ project.feedbackTransport = opts.feedbackTransport;
222
+ if (opts?.voiceCapabilities)
223
+ project.voiceCapabilities = opts.voiceCapabilities;
224
+ if (opts?.sttProvider)
225
+ project.sttProvider = opts.sttProvider;
226
+ if (opts?.ttsProvider)
227
+ project.ttsProvider = opts.ttsProvider;
228
+ return { appName: projectName, app, project };
229
+ }
87
230
  /**
88
231
  * Translate a raw Go-agent error into something a user can act on.
89
232
  *
@@ -143,6 +286,22 @@ class P2PClient {
143
286
  setRelayPassword(password) {
144
287
  this.relayPassword = password;
145
288
  }
289
+ /** Read-only base URL — used by the voice vibe-coding path to probe
290
+ * GET /voice/status before opening the stream. */
291
+ get agentBaseUrl() {
292
+ return this.baseUrl;
293
+ }
294
+ /** WebSocket URL for the agent's voice stream (WS /voice/stream). The
295
+ * voice vibe-coding loop streams mic audio here and receives the
296
+ * transcript + agent task + TTS frames back. */
297
+ voiceStreamUrl() {
298
+ return this.baseUrl.replace(/^http/, 'ws') + '/voice/stream';
299
+ }
300
+ /** Auth headers for the voice WS + status probe — same bearer (and
301
+ * relay password) as every other agent request. */
302
+ voiceAuthHeaders() {
303
+ return this.authHeaders();
304
+ }
146
305
  /** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
147
306
  authHeaders(extra = {}) {
148
307
  const h = { ...extra };
@@ -208,6 +367,45 @@ class P2PClient {
208
367
  const data = await resp.json();
209
368
  return data.session;
210
369
  }
370
+ async getRunnerAuthStatus() {
371
+ const resp = await fetch(`${this.baseUrl}/runner-auth/status`, {
372
+ headers: this.authHeaders(),
373
+ });
374
+ if (!resp.ok) {
375
+ const text = await resp.text().catch(() => '');
376
+ throw new Error(`getRunnerAuthStatus HTTP ${resp.status}: ${text}`);
377
+ }
378
+ const data = await resp.json().catch(() => ({}));
379
+ return Array.isArray(data.runners) ? data.runners : [];
380
+ }
381
+ async getOpenCodeConfig() {
382
+ const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
383
+ headers: this.authHeaders(),
384
+ });
385
+ if (!resp.ok) {
386
+ const text = await resp.text().catch(() => '');
387
+ throw new Error(`getOpenCodeConfig HTTP ${resp.status}: ${text}`);
388
+ }
389
+ const data = await resp.json().catch(() => ({}));
390
+ return (data.config ?? null);
391
+ }
392
+ async saveOpenCodeConfig(patch) {
393
+ try {
394
+ const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
395
+ method: 'POST',
396
+ headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
397
+ body: JSON.stringify(patch),
398
+ });
399
+ const data = await resp.json().catch(() => ({}));
400
+ if (!resp.ok) {
401
+ return { ok: false, error: data.error || `HTTP ${resp.status}` };
402
+ }
403
+ return { ok: true, config: data.config };
404
+ }
405
+ catch (err) {
406
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
407
+ }
408
+ }
211
409
  async capabilitySnapshot() {
212
410
  try {
213
411
  const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
@@ -272,19 +470,24 @@ class P2PClient {
272
470
  }
273
471
  /** Health check — returns true if the agent is reachable. */
274
472
  async health() {
473
+ let timeoutId = null;
275
474
  try {
276
475
  const controller = new AbortController();
277
- const timeoutId = setTimeout(() => controller.abort(), 3000);
476
+ timeoutId = setTimeout(() => controller.abort(), 3000);
477
+ unrefTimer(timeoutId);
278
478
  const response = await fetch(`${this.baseUrl}/health`, {
279
479
  method: 'GET',
280
480
  signal: controller.signal,
281
481
  });
282
- clearTimeout(timeoutId);
283
482
  return response.ok;
284
483
  }
285
484
  catch {
286
485
  return false;
287
486
  }
487
+ finally {
488
+ if (timeoutId)
489
+ clearTimeout(timeoutId);
490
+ }
288
491
  }
289
492
  /** Get agent info (hostname, version, platform). */
290
493
  async info() {
@@ -390,6 +593,10 @@ class P2PClient {
390
593
  s2sReady: data.s2sReady ?? false,
391
594
  sttProvider: data.sttProvider ?? undefined,
392
595
  sttReady: data.sttReady ?? false,
596
+ ttsProvider: data.ttsProvider ?? undefined,
597
+ ttsReady: data.ttsReady ?? false,
598
+ enabled: data.enabled ?? false,
599
+ defaultProject: data.defaultProject ?? undefined,
393
600
  };
394
601
  }
395
602
  /**
@@ -429,6 +636,78 @@ class P2PClient {
429
636
  * via the BlackBox command channel.
430
637
  * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
431
638
  */
639
+ /**
640
+ * Read the dev server's state so the overlay can decide WHICH reload
641
+ * actions to offer, and disable the rest with a reason.
642
+ *
643
+ * Returns null when the machine cannot be reached at all — which the
644
+ * caller must render as "not connected", never as "no dev server".
645
+ * Those are two different problems with two different fixes.
646
+ */
647
+ async getDevServerStatus() {
648
+ try {
649
+ const resp = await fetch(`${this.baseUrl}${endpoints_1.AGENT_ENDPOINTS.devStatus}`, {
650
+ headers: this.authHeaders(),
651
+ });
652
+ if (!resp.ok)
653
+ return null;
654
+ const data = await resp.json().catch(() => ({}));
655
+ return {
656
+ running: data.running === true,
657
+ building: data.building === true,
658
+ framework: typeof data.framework === 'string' ? data.framework : undefined,
659
+ };
660
+ }
661
+ catch {
662
+ return null;
663
+ }
664
+ }
665
+ /**
666
+ * Trigger a reload with an EXPLICIT fast/full mode — no bundle fallback.
667
+ *
668
+ * `reloadApp('dev')` silently falls through to a bundle rebuild when the
669
+ * dev server is down, which is right for a one-button UX and wrong the
670
+ * moment the user picks between two named actions: someone who pressed
671
+ * "Full Reload" must not get a 60-second bundle rebuild without being
672
+ * told. So this method reports the failure instead, with a named cause.
673
+ *
674
+ * Auth: the SAME bearer used for the feedback POST. `/dev/reload` is
675
+ * registered under `authSDKOrGuest` in desktop/agent/httpserver.go and is
676
+ * already in the `guest-reload` SDK-token scope list — nothing widens.
677
+ */
678
+ async reloadWithMode(mode, snapshot) {
679
+ if (mode === 'bundle')
680
+ return this.reloadApp('bundle');
681
+ let resp;
682
+ try {
683
+ resp = await fetch(`${this.baseUrl}${reloadActions_1.RELOAD_PATH}`, {
684
+ method: 'POST',
685
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
686
+ body: JSON.stringify({ mode }),
687
+ });
688
+ }
689
+ catch (err) {
690
+ throw new Error((0, reloadActions_1.describeReloadFailure)(0, err instanceof Error ? err.message : '', snapshot));
691
+ }
692
+ if (!resp.ok) {
693
+ const text = await resp.text().catch(() => '');
694
+ throw new Error((0, reloadActions_1.describeReloadFailure)(resp.status, text, snapshot));
695
+ }
696
+ const payload = await resp.json().catch(() => ({}));
697
+ const nativeChangesDetected = payload.nativeChangesDetected === true;
698
+ return {
699
+ ok: true,
700
+ mode: 'dev',
701
+ acknowledged: true,
702
+ nativeChangesDetected,
703
+ changeClass: typeof payload.changeClass === 'string' ? payload.changeClass : undefined,
704
+ message: nativeChangesDetected
705
+ ? 'Reload accepted, but native files changed — a rebuild is required.'
706
+ : mode === 'full'
707
+ ? 'Full reload requested.'
708
+ : 'Hot reload requested.',
709
+ };
710
+ }
432
711
  async reloadApp(mode = 'bundle', opts) {
433
712
  // Default path: always rebuild a fresh Hermes bundle.
434
713
  //
@@ -84,6 +84,8 @@ class ShakeDetector {
84
84
  * the `ShakeEvent` name a handful of third-party shake libraries emit.
85
85
  */
86
86
  subscribeDevMenu(onShake) {
87
+ if (typeof react_native_1.DeviceEventEmitter?.addListener !== 'function')
88
+ return;
87
89
  const eventName = react_native_1.Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
88
90
  this.devMenuSub = react_native_1.DeviceEventEmitter.addListener(eventName, () => {
89
91
  this.fire(onShake);
@@ -15,6 +15,11 @@ interface Props {
15
15
  /** Called when the user taps Reload after a task completes — uses
16
16
  * P2PClient.reloadApp() with the active project context. */
17
17
  onReload?: () => Promise<void>;
18
+ /** Optional context forwarded to the voice stream so the agent runs
19
+ * the task against the right project / runner / model. */
20
+ project?: string;
21
+ model?: string;
22
+ runner?: string;
18
23
  }
19
- export declare function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, }: Props): React.JSX.Element;
24
+ export declare function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, model, runner, }: Props): React.JSX.Element;
20
25
  export {};