@frockbot/plugin-voice 0.3.21 → 0.3.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-voice",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,12 +26,12 @@
26
26
  "typecheck": "vue-tsc --noEmit -p tsconfig.json"
27
27
  },
28
28
  "dependencies": {
29
- "@frockbot/client-core": "0.3.21",
30
- "@frockbot/client-ui": "0.3.21",
31
- "@frockbot/kernel-contracts": "0.3.21",
32
- "@frockbot/plugin-flock": "0.3.21",
33
- "@frockbot/plugin-shell": "0.3.21",
34
- "@frockbot/protocol": "0.3.21",
29
+ "@frockbot/client-core": "0.3.23",
30
+ "@frockbot/client-ui": "0.3.23",
31
+ "@frockbot/kernel-contracts": "0.3.23",
32
+ "@frockbot/plugin-flock": "0.3.23",
33
+ "@frockbot/plugin-shell": "0.3.23",
34
+ "@frockbot/protocol": "0.3.23",
35
35
  "cordis": "4.0.0-rc.8",
36
36
  "vue": "3.5.41"
37
37
  },
@@ -5,6 +5,7 @@ import {
5
5
  type ClientSurfaceRegistry,
6
6
  } from "@frockbot/client-core";
7
7
  import { computed, ref } from "vue";
8
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
8
9
  import { voiceClientPlugin } from "./index.js";
9
10
  import { voiceClientStateKey, type VoiceClientStateV1 } from "./state.js";
10
11
 
@@ -26,9 +27,22 @@ function surfaceRegistry(): ClientSurfaceRegistry {
26
27
 
27
28
  function mount(
28
29
  hostedRequest: NonNullable<ClientPluginContext["transport"]["hostedRequest"]>,
29
- ): { calls: string[]; state: { value: VoiceClientStateV1 } } {
30
+ ): {
31
+ calls: string[];
32
+ state: { value: VoiceClientStateV1 };
33
+ reloadHolds: () => number;
34
+ } {
30
35
  const calls: string[] = [];
31
36
  const surfaces = surfaceRegistry();
37
+ const web = ref({
38
+ reloadHolds: 0,
39
+ holdReload: () => {
40
+ web.value.reloadHolds += 1;
41
+ return () => {
42
+ web.value.reloadHolds -= 1;
43
+ };
44
+ },
45
+ });
32
46
  let state: unknown;
33
47
  const context: ClientPluginContext = {
34
48
  transport: {
@@ -40,6 +54,7 @@ function mount(
40
54
  },
41
55
  inject: (key) => {
42
56
  if (key === clientSurfaceRegistryKey) return surfaces as never;
57
+ if (key === frockBotWebDataKey) return web as never;
43
58
  throw new Error("unexpected client provider");
44
59
  },
45
60
  provide: (key, value) => {
@@ -52,10 +67,23 @@ function mount(
52
67
  return {
53
68
  calls,
54
69
  state: state as { value: VoiceClientStateV1 },
70
+ reloadHolds: () => web.value.reloadHolds,
55
71
  };
56
72
  }
57
73
 
58
74
  describe("Voice client contribution", () => {
75
+ test("holds the shell's reload while a session is live", () => {
76
+ const mounted = mount(() => Promise.reject(new Error("unauthenticated")));
77
+
78
+ expect(mounted.reloadHolds()).toBe(0);
79
+ mounted.state.value.status = "listening";
80
+ expect(mounted.reloadHolds()).toBe(1);
81
+ mounted.state.value.status = "speaking";
82
+ expect(mounted.reloadHolds()).toBe(1);
83
+ mounted.state.value.status = "offline";
84
+ expect(mounted.reloadHolds()).toBe(0);
85
+ });
86
+
59
87
  test("does not read User voice state before authenticated chrome mounts", () => {
60
88
  const mounted = mount(() => Promise.reject(new Error("unauthenticated")));
61
89
 
@@ -11,8 +11,9 @@ import {
11
11
  type VoiceMicrophoneV1,
12
12
  } from "@frockbot/plugin-shell/client/voice-microphone";
13
13
  import { showClientNotificationV1 } from "@frockbot/plugin-shell/client/notify";
14
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
14
15
  import { VOICE_ASSISTANT_INPUT_SAMPLE_RATE_V1 } from "@frockbot/protocol";
15
- import { ref } from "vue";
16
+ import { ref, watch } from "vue";
16
17
  import {
17
18
  decodeVoiceAssistantViewV1,
18
19
  VOICE_MAX_TOOL_CALLS_V1,
@@ -299,6 +300,28 @@ export const voiceClientPlugin: ClientPlugin = (ctx) => {
299
300
  if (state.value.status === "offline") void refresh();
300
301
  }, VOICE_PENDING_POLL_INTERVAL_MS_V1);
301
302
 
303
+ /*
304
+ * A live session is work a reload would throw away, and the Voice surface
305
+ * is a panel rather than an overlay, so the shell cannot see it any other
306
+ * way. Holding is how a Package says "not right now" without the shell
307
+ * knowing anything about Voice.
308
+ */
309
+ const web = ctx.inject(frockBotWebDataKey);
310
+ let releaseHold: (() => void) | undefined;
311
+ const stopHold = watch(
312
+ () => state.value.status !== "offline",
313
+ (live) => {
314
+ if (live && !releaseHold) releaseHold = web.value.holdReload();
315
+ else if (!live && releaseHold) {
316
+ releaseHold();
317
+ releaseHold = undefined;
318
+ }
319
+ },
320
+ // Synchronous: the hold has to be up before anything can act on the
321
+ // status change, or a reload could land in the gap.
322
+ { immediate: true, flush: "sync" },
323
+ );
324
+
302
325
  return [
303
326
  ctx.provide(voiceClientStateKey, state),
304
327
  surfaces.register({
@@ -313,6 +336,9 @@ export const voiceClientPlugin: ClientPlugin = (ctx) => {
313
336
  component: VoiceToggle,
314
337
  }),
315
338
  () => {
339
+ stopHold();
340
+ releaseHold?.();
341
+ releaseHold = undefined;
316
342
  clearInterval(pendingPoll);
317
343
  attempt += 1;
318
344
  session?.close();