@alexkroman1/aai-ui 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +257 -0
  2. package/dist/_colors-CpZO-88A.js +78 -0
  3. package/dist/_module-url-C13kAJ87.js +15 -0
  4. package/dist/_page-shell.d.ts +11 -0
  5. package/dist/_recover-run.d.ts +79 -0
  6. package/dist/_repeat-until.d.ts +30 -0
  7. package/dist/_run-controls.d.ts +33 -0
  8. package/dist/_submission-state.d.ts +92 -0
  9. package/dist/_tap-to-talk-state.d.ts +88 -0
  10. package/dist/_utils-Mi-SCFA1.js +47 -0
  11. package/dist/_utils.d.ts +26 -0
  12. package/dist/_web-storage.d.ts +43 -0
  13. package/dist/_workflow-api-ref.d.ts +37 -0
  14. package/dist/_workflow-files.d.ts +34 -0
  15. package/dist/_workflow-test-defs.d.ts +38 -0
  16. package/dist/aai-logo-D8pMVn9_.js +85 -0
  17. package/dist/agent-state-labels.d.ts +65 -0
  18. package/dist/agent-state.d.ts +108 -0
  19. package/dist/audio/capture.d.ts +44 -0
  20. package/dist/audio/index.d.ts +18 -0
  21. package/dist/audio/index.js +431 -0
  22. package/dist/audio/preconnect.d.ts +87 -0
  23. package/dist/audio/voice-io.d.ts +112 -0
  24. package/dist/build-default-client.d.ts +1 -0
  25. package/dist/chat-view-BCbI7gD4.js +396 -0
  26. package/dist/client-config-C7fQrioM.js +139 -0
  27. package/dist/client-config.d.ts +119 -0
  28. package/dist/client-dir.d.ts +44 -0
  29. package/dist/client-dir.js +58 -0
  30. package/dist/client-identity.d.ts +131 -0
  31. package/dist/client-tool.d.ts +51 -0
  32. package/dist/components/_colors.d.ts +73 -0
  33. package/dist/components/_form-readiness.d.ts +56 -0
  34. package/dist/components/_form-values.d.ts +19 -0
  35. package/dist/components/aai-logo.d.ts +12 -0
  36. package/dist/components/audio-result.d.ts +94 -0
  37. package/dist/components/auto-scroll.d.ts +74 -0
  38. package/dist/components/bullet-list.d.ts +74 -0
  39. package/dist/components/button.d.ts +58 -0
  40. package/dist/components/button.js +85 -0
  41. package/dist/components/chat-view.d.ts +35 -0
  42. package/dist/components/chat-view.js +3 -0
  43. package/dist/components/console-shell.d.ts +85 -0
  44. package/dist/components/controls.d.ts +33 -0
  45. package/dist/components/controls.js +49 -0
  46. package/dist/components/conversation-view.d.ts +130 -0
  47. package/dist/components/eyebrow.d.ts +12 -0
  48. package/dist/components/facts.d.ts +81 -0
  49. package/dist/components/form-fields.d.ts +142 -0
  50. package/dist/components/form-types.d.ts +73 -0
  51. package/dist/components/form.d.ts +96 -0
  52. package/dist/components/markdown.d.ts +57 -0
  53. package/dist/components/message-list.d.ts +48 -0
  54. package/dist/components/message-list.js +3 -0
  55. package/dist/components/session-controls.d.ts +125 -0
  56. package/dist/components/session-error-banner.d.ts +69 -0
  57. package/dist/components/session-state-dot.d.ts +111 -0
  58. package/dist/components/sidebar-layout.d.ts +42 -0
  59. package/dist/components/sidebar-layout.js +56 -0
  60. package/dist/components/start-screen.d.ts +37 -0
  61. package/dist/components/start-screen.js +71 -0
  62. package/dist/components/tool-call-block.d.ts +28 -0
  63. package/dist/components/tool-call-block.js +3 -0
  64. package/dist/components/tool-call-row.d.ts +75 -0
  65. package/dist/components/tool-config-context.d.ts +22 -0
  66. package/dist/components/upload-progress.d.ts +76 -0
  67. package/dist/components/url-chips.d.ts +29 -0
  68. package/dist/components/workflow-fields.d.ts +46 -0
  69. package/dist/components/workflow-pending-note.d.ts +81 -0
  70. package/dist/components/workflow-progress.d.ts +81 -0
  71. package/dist/components/workflow-run-error.d.ts +48 -0
  72. package/dist/components/workflow-run-panel.d.ts +79 -0
  73. package/dist/context.d.ts +288 -0
  74. package/dist/context.js +382 -0
  75. package/dist/conversation-log.d.ts +88 -0
  76. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  77. package/dist/default-client/assets/audio-cGFGID-2.js +1 -0
  78. package/dist/default-client/assets/capture-processor-BUABoo3_.js +90 -0
  79. package/dist/default-client/assets/index-8ihb1Urm.css +2 -0
  80. package/dist/default-client/assets/index-BQURVARl.js +126 -0
  81. package/dist/default-client/assets/playback-processor-ABOa11j0.js +314 -0
  82. package/dist/default-client/favicon.ico +0 -0
  83. package/dist/default-client/index.html +15 -0
  84. package/dist/default-client.d.ts +23 -0
  85. package/dist/define-client.d.ts +162 -26
  86. package/dist/define-client.js +201 -55
  87. package/dist/eyebrow-UfmSz9yy.js +27 -0
  88. package/dist/hooks-DN4gHYsj.js +182 -0
  89. package/dist/hooks.d.ts +172 -0
  90. package/dist/hooks.js +3 -0
  91. package/dist/hooks.test-d.d.ts +14 -0
  92. package/dist/inbox-protocol.d.ts +128 -0
  93. package/dist/inbox.d.ts +132 -0
  94. package/dist/index.d.ts +129 -32
  95. package/dist/index.js +5503 -18
  96. package/dist/internal.d.ts +43 -0
  97. package/dist/internal.js +8 -0
  98. package/dist/linked-client.d.ts +70 -0
  99. package/dist/message-list-DHZcLhRA.js +682 -0
  100. package/dist/notice-player.d.ts +70 -0
  101. package/dist/page.d.ts +114 -0
  102. package/dist/phone.d.ts +50 -0
  103. package/dist/route-fetch.d.ts +79 -0
  104. package/dist/session/audio-effects.d.ts +31 -0
  105. package/dist/session/audio-setup.d.ts +57 -0
  106. package/dist/session/audio-state.d.ts +157 -0
  107. package/dist/session/browser-session.d.ts +30 -0
  108. package/dist/session/close.d.ts +26 -0
  109. package/dist/session/dial.d.ts +65 -0
  110. package/dist/session/handshake.d.ts +60 -0
  111. package/dist/session/index.d.ts +17 -0
  112. package/dist/session/index.js +2 -0
  113. package/dist/session/messages.d.ts +63 -0
  114. package/dist/session/mic.d.ts +64 -0
  115. package/dist/session/preconnect.d.ts +48 -0
  116. package/dist/session/reconnect.d.ts +25 -0
  117. package/dist/session/resume-store.d.ts +57 -0
  118. package/dist/session/state.d.ts +146 -0
  119. package/dist/session/ticket.d.ts +44 -0
  120. package/dist/session/types.d.ts +476 -0
  121. package/dist/session/url.d.ts +17 -0
  122. package/dist/session/user-turn.d.ts +55 -0
  123. package/dist/session-CE0Ipsb3.js +2129 -0
  124. package/dist/stored-value.d.ts +103 -0
  125. package/dist/tool-call-block-uXtMOUWN.js +204 -0
  126. package/dist/tool-config-context-Es4YUzV2.js +19 -0
  127. package/dist/types.d.ts +272 -34
  128. package/dist/types.js +35 -4
  129. package/dist/upload/files.d.ts +83 -0
  130. package/dist/upload/index.d.ts +15 -0
  131. package/dist/upload/pause.d.ts +31 -0
  132. package/dist/upload/recall.d.ts +67 -0
  133. package/dist/upload/report.d.ts +26 -0
  134. package/dist/upload/session.d.ts +99 -0
  135. package/dist/url-chips-oyy_Eiqh.js +239 -0
  136. package/dist/use-client-runs.d.ts +87 -0
  137. package/dist/use-conversation-log.d.ts +86 -0
  138. package/dist/use-conversation.d.ts +131 -0
  139. package/dist/use-copy.d.ts +74 -0
  140. package/dist/use-download-url.d.ts +84 -0
  141. package/dist/use-flash.d.ts +61 -0
  142. package/dist/use-inbox.d.ts +101 -0
  143. package/dist/use-push-to-talk.d.ts +105 -0
  144. package/dist/use-route-mutation.d.ts +113 -0
  145. package/dist/use-route.d.ts +82 -0
  146. package/dist/use-run-key.d.ts +133 -0
  147. package/dist/use-session-controls.d.ts +69 -0
  148. package/dist/use-session-id.d.ts +57 -0
  149. package/dist/use-tap-to-talk.d.ts +138 -0
  150. package/dist/use-user-transcript-uyHhzy4d.js +73 -0
  151. package/dist/use-user-transcript.d.ts +75 -0
  152. package/dist/use-workflow-form.d.ts +285 -0
  153. package/dist/use-workflow-progress.d.ts +137 -0
  154. package/dist/use-workflow-run-DdhWmbx-.js +697 -0
  155. package/dist/use-workflow-run.d.ts +100 -0
  156. package/dist/use-workflow-runs.d.ts +71 -0
  157. package/dist/use-workflow-stream.d.ts +154 -0
  158. package/dist/use-workflows.d.ts +77 -0
  159. package/dist/workflow-client.d.ts +136 -0
  160. package/dist/workflow-def-types.d.ts +31 -0
  161. package/dist/workflow-events.d.ts +39 -0
  162. package/dist/workflow-field-kind.d.ts +95 -0
  163. package/dist/workflow-status-labels.d.ts +35 -0
  164. package/dist/worklets/_module-url.d.ts +10 -0
  165. package/dist/worklets/_playback-bench-harness.d.ts +181 -0
  166. package/dist/worklets/_playback-bench-host.d.ts +63 -0
  167. package/dist/worklets/_playback-bench-page.d.ts +65 -0
  168. package/dist/worklets/_tts-trace-harness.d.ts +142 -0
  169. package/dist/worklets/_worklet-test-utils.d.ts +27 -0
  170. package/dist/worklets/capture-processor.d.ts +4 -2
  171. package/dist/worklets/capture-processor.js +73 -33
  172. package/dist/worklets/playback-processor.d.ts +4 -2
  173. package/dist/worklets/playback-processor.js +258 -42
  174. package/package.json +58 -31
  175. package/styles.css +152 -30
  176. package/dist/_components/app.d.ts +0 -23
  177. package/dist/_components/app.js +0 -41
  178. package/dist/_components/button.d.ts +0 -50
  179. package/dist/_components/button.js +0 -46
  180. package/dist/_components/chat-view.d.ts +0 -29
  181. package/dist/_components/chat-view.js +0 -61
  182. package/dist/_components/controls.d.ts +0 -18
  183. package/dist/_components/controls.js +0 -37
  184. package/dist/_components/error-banner.d.ts +0 -21
  185. package/dist/_components/error-banner.js +0 -27
  186. package/dist/_components/message-bubble.d.ts +0 -23
  187. package/dist/_components/message-bubble.js +0 -35
  188. package/dist/_components/message-list.d.ts +0 -21
  189. package/dist/_components/message-list.js +0 -76
  190. package/dist/_components/sidebar-layout.d.ts +0 -21
  191. package/dist/_components/sidebar-layout.js +0 -36
  192. package/dist/_components/start-screen.d.ts +0 -26
  193. package/dist/_components/start-screen.js +0 -51
  194. package/dist/_components/state-indicator.d.ts +0 -29
  195. package/dist/_components/state-indicator.js +0 -37
  196. package/dist/_components/thinking-indicator.d.ts +0 -16
  197. package/dist/_components/thinking-indicator.js +0 -34
  198. package/dist/_components/tool-call-block.d.ts +0 -27
  199. package/dist/_components/tool-call-block.js +0 -118
  200. package/dist/_components/tool-icons.d.ts +0 -17
  201. package/dist/_components/tool-icons.js +0 -128
  202. package/dist/_components/transcript.d.ts +0 -25
  203. package/dist/_components/transcript.js +0 -35
  204. package/dist/audio.d.ts +0 -45
  205. package/dist/audio.js +0 -121
  206. package/dist/client-context.d.ts +0 -33
  207. package/dist/client-context.js +0 -15
  208. package/dist/client-handler.d.ts +0 -43
  209. package/dist/session-CWB7vmqz.js +0 -429
  210. package/dist/session.d.ts +0 -69
  211. package/dist/session.js +0 -2
  212. package/dist/signals.d.ts +0 -120
  213. package/dist/signals.js +0 -161
  214. package/dist/tsdown.config.d.ts +0 -2
  215. package/dist/types.test-d.d.ts +0 -7
@@ -1,57 +1,97 @@
1
- //#region worklets/capture-processor.ts
2
- const script = new Blob([`
1
+ import { MIC_BUFFER_SECONDS, MIC_SILENCE_PROBE_MS } from "../types.js";
2
+ import { t as workletModuleUrl } from "../_module-url-C13kAJ87.js";
3
+ //#region src/worklets/capture-processor.ts
4
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
5
+ const captureProcessorSource = `
3
6
  class CaptureProcessor extends AudioWorkletProcessor {
4
7
  constructor(options) {
5
8
  super();
6
9
  this.recording = false;
7
10
  const opts = options.processorOptions || {};
8
- this.fromRate = opts.contextRate || sampleRate;
9
- this.toRate = opts.sttSampleRate || sampleRate;
10
- this.ratio = this.fromRate / this.toRate;
11
- this.needsResample = this.fromRate !== this.toRate;
11
+ // The context runs at the STT rate, so this is both the input and the
12
+ // output rate — there is nothing to convert.
13
+ this.rate = opts.sampleRate || sampleRate;
14
+ // Int16 accumulation buffer: flushed to the main thread as one transferred
15
+ // ArrayBuffer once ~bufferSeconds of samples are batched. Sized 2x the
16
+ // flush target so a whole render quantum always fits before flushing.
17
+ this.targetSamples = Math.max(1, Math.round(this.rate * (opts.bufferSeconds || ${MIC_BUFFER_SECONDS})));
18
+ this.pending = new Int16Array(this.targetSamples * 2);
19
+ this.pendingLen = 0;
20
+ // Dead-mic probe: samples left to inspect before concluding the device
21
+ // delivers nothing but digital silence. Only consumed while recording, so
22
+ // the cost disappears after the window (or after the first real sample).
23
+ this.probeSamplesLeft = Math.round(
24
+ (this.rate * (opts.silenceProbeMs ?? ${MIC_SILENCE_PROBE_MS})) / 1000,
25
+ );
12
26
  this.port.onmessage = (e) => {
13
27
  if (e.data.event === 'start') this.recording = true;
14
- else if (e.data.event === 'stop') this.recording = false;
28
+ else if (e.data.event === 'stop') {
29
+ // Final flush so the tail of speech isn't dropped on close, then ack
30
+ // so the host knows the tail chunk (if any) has been posted and it is
31
+ // safe to tear the context down.
32
+ this.flush();
33
+ this.recording = false;
34
+ this.port.postMessage({ event: 'stopped' });
35
+ }
15
36
  };
16
37
  }
17
38
 
18
- resample(input) {
19
- const ratio = this.ratio;
20
- const outLen = Math.ceil(input.length / ratio);
21
- const out = new Float32Array(outLen);
22
- for (let i = 0; i < outLen; i++) {
23
- const srcIdx = i * ratio;
24
- const idx = srcIdx | 0;
25
- const frac = srcIdx - idx;
26
- const a = input[idx];
27
- const b = idx + 1 < input.length ? input[idx + 1] : a;
28
- out[i] = a + frac * (b - a);
39
+ // Convert Float32 -> Int16 and append to the pending batch. Writes through
40
+ // an Int16Array directly (assignment truncates like DataView.setInt16).
41
+ accumulate(samples) {
42
+ let buf = this.pending;
43
+ if (this.pendingLen + samples.length > buf.length) {
44
+ // Defensive: only reachable if a render quantum outproduces the 1x
45
+ // headroom above the flush target (never with 128-sample quanta).
46
+ const grown = new Int16Array((this.pendingLen + samples.length) * 2);
47
+ grown.set(buf.subarray(0, this.pendingLen));
48
+ this.pending = grown;
49
+ buf = grown;
50
+ }
51
+ for (let i = 0; i < samples.length; i++) {
52
+ const s = Math.max(-1, Math.min(1, samples[i]));
53
+ buf[this.pendingLen++] = s < 0 ? s * 0x8000 : s * 0x7fff;
54
+ }
55
+ }
56
+
57
+ // Post the batched samples as one transferred ArrayBuffer and reset.
58
+ flush() {
59
+ if (this.pendingLen === 0) return;
60
+ const buffer = this.pending.buffer.slice(0, this.pendingLen * 2);
61
+ this.pendingLen = 0;
62
+ this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
63
+ }
64
+
65
+ // Watch the first window of input for any nonzero sample. One is enough to
66
+ // prove the device is live — a real mic in a quiet room still carries a
67
+ // noise floor, so all-zeros means muted, wrong input, or no input at all.
68
+ probeForSilence(channel) {
69
+ for (let i = 0; i < channel.length; i++) {
70
+ if (channel[i] !== 0) {
71
+ this.probeSamplesLeft = 0;
72
+ return;
73
+ }
74
+ }
75
+ this.probeSamplesLeft -= channel.length;
76
+ if (this.probeSamplesLeft <= 0) {
77
+ this.port.postMessage({ event: 'silent' });
29
78
  }
30
- return out;
31
79
  }
32
80
 
33
81
  process(inputs) {
34
82
  const input = inputs[0];
35
83
  if (!input || !input[0] || !this.recording) return true;
36
84
 
37
- const raw = input[0];
38
- const samples = this.needsResample ? this.resample(raw) : raw;
85
+ if (this.probeSamplesLeft > 0) this.probeForSilence(input[0]);
39
86
 
40
- // Convert Float32 -> Int16
41
- const buffer = new ArrayBuffer(samples.length * 2);
42
- const view = new DataView(buffer);
43
- for (let i = 0; i < samples.length; i++) {
44
- const s = Math.max(-1, Math.min(1, samples[i]));
45
- view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
46
- }
47
-
48
- this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
87
+ this.accumulate(input[0]);
88
+ if (this.pendingLen >= this.targetSamples) this.flush();
49
89
  return true;
50
90
  }
51
91
  }
52
92
 
53
93
  registerProcessor('capture-processor', CaptureProcessor);
54
- `], { type: "application/javascript" });
55
- const src = URL.createObjectURL(script);
94
+ `;
95
+ var capture_processor_default = workletModuleUrl(captureProcessorSource);
56
96
  //#endregion
57
- export { src as default };
97
+ export { captureProcessorSource, capture_processor_default as default };
@@ -1,2 +1,4 @@
1
- declare const src: string;
2
- export default src;
1
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
2
+ export declare const playbackProcessorSource = "\nclass PlaybackProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n const opts = options.processorOptions || {};\n // The node's context IS the playback context (audio/voice-io.ts asserts its rate),\n // so the worklet-global sampleRate is authoritative; the option exists\n // for the node-less test harness.\n const rate = opts.sampleRate ?? sampleRate;\n // Kept, because every derived quantity below reads it and `bufferedMs` in\n // process() is the one that used to read the worklet global instead. Those\n // agree in production (the option is unset, so `rate` IS `sampleRate`) and\n // diverge only in the node-less harness \u2014 which is the one place a spec\n // could ever assert on the reported backlog, so the divergence made the\n // number untestable rather than wrong.\n this.rate = rate;\n // The one fill target \u2014 for the start of a turn and for recovery after an\n // underrun alike. If 'done' arrives first (short utterance), start\n // immediately instead of waiting for audio that is never coming.\n this.fillSamples = Math.floor((rate * (opts.fillMs ?? 200)) / 1000);\n // Concealment source: a ring of the most recently played samples, looped\n // under a decaying gain to cover a gap. Sized to the fade window, with a\n // per-sample decay that reaches the floor exactly at its end.\n this.concealCapacity = Math.max(1, Math.floor((rate * 40) / 1000));\n this.concealBuf = new Float32Array(this.concealCapacity);\n this.concealDecay = Math.exp(Math.log(0.001) / this.concealCapacity);\n // Float32 ring buffer \u2014 PLAYBACK_BUFFER_SECONDS at the context sample\n // rate. Allocated once for the node's lifetime; per-turn state resets via\n // resetTurn(). writePos and readPos are absolute (monotonic) sample\n // counts; the buffer is indexed modulo capacity so a longer reply keeps\n // playing instead of writing past the end and going silent.\n this.capacity = rate * 60;\n this.samples = new Float32Array(this.capacity);\n // Cadence for the 'progress' report in process(), counted in samples so\n // the hot path never reads a clock. Survives resetTurn(): the host clamps\n // upward only, so a stale count costs at most one extra report.\n this.reportIntervalSamples = Math.floor((rate * 500) / 1000);\n this.sinceReportSamples = 0;\n // Platform endianness probe: the wire format is PCM16 little-endian, so\n // the Int16Array fast path in ingestBytes is only valid on LE hosts\n // (every shipping browser target; the DataView path is the fallback).\n this.littleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;\n this.resetTurn();\n\n this.port.onmessage = (e) => {\n const d = e.data;\n if (d.event === 'write') {\n this.ingestBytes(d.buffer);\n } else if (d.event === 'interrupt') {\n // Applied eagerly, not deferred to the next process(): onmessage and\n // process() never interleave (one audio thread), and a deferred\n // interrupt would let 'write'/'done' frames for the NEXT turn\n // coalesce in ahead of it and be wiped by the reset along with the\n // cancelled turn's audio.\n this.stopTurn('interrupt');\n } else if (d.event === 'done') {\n this.isDone = true;\n // Echoed back on this turn's 'stop' so the host can tell WHICH turn\n // drained: a stop already in flight when a barge-in lands would\n // otherwise settle the next turn's done() the moment it arrives.\n this.doneTurn = d.turn ?? null;\n }\n };\n }\n\n // Reset per-turn state so the node is reusable across replies without\n // reallocating the sample buffer or re-instantiating the worklet.\n resetTurn() {\n this.isDone = false;\n // Host turn id carried by this turn's 'done' (null until one arrives).\n this.doneTurn = null;\n this.playing = false;\n // Whether any real audio has been rendered this turn. Separates a turn's\n // pre-roll (nothing to extrapolate from, and not a defect) from a\n // mid-turn underrun.\n this.hasPlayed = false;\n // Carry-over byte for split samples across chunks\n this.carry = null;\n this.writePos = 0;\n this.readPos = 0;\n // Concealment ring state and the current fade position.\n this.concealLen = 0;\n this.concealWrite = 0;\n this.concealPos = 0;\n this.concealGain = 1;\n // Episode flags, so a multi-quantum gap counts as one event.\n this.concealing = false;\n this.concealedSilence = false;\n // Reported to the host on 'stop'. A fresh object per turn: the one just\n // posted must not be mutated by the next turn.\n this.stats = {\n concealedSamples: 0,\n silentConcealedSamples: 0,\n concealmentEvents: 0,\n silentConcealmentEvents: 0,\n };\n }\n\n // End the current turn: notify the host and rearm for the next reply.\n // Must NOT return false from process() \u2014 a processor that stops is dead\n // for good, forcing a new node (and buffer) per reply.\n // `reason` ('interrupt' | 'done') tells the host which turn boundary this\n // stop belongs to \u2014 interrupt-stops are dropped host-side, flush() having\n // already settled that turn \u2014 and `turn` names WHICH turn drained (the id\n // this turn's 'done' carried), so a drain-stop still in flight when a\n // barge-in lands cannot settle the next turn's done() early.\n stopTurn(reason) {\n this.port.postMessage({ event: 'stop', reason, turn: this.doneTurn, stats: this.stats });\n this.resetTurn();\n }\n\n // Cover a quantum (from `start`) where real audio should have been.\n //\n // Before the turn's first samples there is nothing to extrapolate from, so\n // the gap is plain silence and counted as nothing \u2014 WebRTC likewise only\n // counts concealment once playout has begun. After that, loop the retained\n // tail under a decaying gain: a hard zero-fill is a discontinuity mid-word,\n // which is the click that makes a brief stall sound like breakage.\n coverGap(out, start) {\n if (!this.hasPlayed) {\n out.fill(0, start);\n return;\n }\n if (!this.concealing) {\n this.concealing = true;\n this.concealedSilence = false;\n this.stats.concealmentEvents++;\n }\n const total = out.length - start;\n const len = this.concealLen;\n let silent = 0;\n // Second condition: once the fade has decayed to the floor, every sample\n // the loop would emit is 0 anyway \u2014 bulk-fill instead of running 128\n // branchy iterations per quantum for the whole tail of a long stall.\n if (len === 0 || this.concealGain < 0.001) {\n out.fill(0, start);\n silent = total;\n } else {\n let g = this.concealGain;\n for (let i = start; i < out.length; i++) {\n if (g < 0.001) {\n // The fade has run out: keep counting the gap, but stop looping a\n // fragment that is now inaudible anyway.\n out[i] = 0;\n silent++;\n continue;\n }\n out[i] = this.concealBuf[this.concealPos] * g;\n this.concealPos = this.concealPos + 1 === len ? 0 : this.concealPos + 1;\n g *= this.concealDecay;\n }\n this.concealGain = g;\n }\n this.stats.concealedSamples += total;\n if (silent > 0) {\n this.stats.silentConcealedSamples += silent;\n if (!this.concealedSilence) {\n this.concealedSilence = true;\n this.stats.silentConcealmentEvents++;\n }\n }\n }\n\n // Retain the tail of a rendered quantum as the next gap's concealment\n // source, and close any episode the real audio just ended.\n rememberTail(out, n) {\n const cap = this.concealCapacity;\n const take = Math.min(n, cap);\n // Bulk copies (this runs on every cleanly rendered quantum): the tail is\n // one contiguous source run, landing in at most two ring runs.\n const tail = out.subarray(n - take, n);\n const first = Math.min(take, cap - this.concealWrite);\n this.concealBuf.set(tail.subarray(0, first), this.concealWrite);\n if (take > first) this.concealBuf.set(tail.subarray(first), 0);\n this.concealWrite = (this.concealWrite + take) % cap;\n this.concealLen = Math.min(cap, this.concealLen + take);\n // Read the loop oldest-first; once the ring is full the write cursor is\n // the oldest retained sample.\n this.concealPos = this.concealLen === cap ? this.concealWrite : 0;\n this.concealing = false;\n this.concealGain = 1;\n }\n\n ingestBytes(uint8) {\n let bytes = uint8;\n\n if (this.carry !== null) {\n const merged = new Uint8Array(1 + bytes.length);\n merged[0] = this.carry;\n merged.set(bytes, 1);\n bytes = merged;\n this.carry = null;\n }\n\n if (bytes.length % 2 !== 0) {\n this.carry = bytes[bytes.length - 1];\n bytes = bytes.subarray(0, bytes.length - 1);\n }\n\n if (bytes.length === 0) return;\n const numSamples = bytes.length / 2;\n const cap = this.capacity;\n const samples = this.samples;\n if (this.littleEndian && (bytes.byteOffset & 1) === 0) {\n // Fast path: 2-byte-aligned LE bytes wrap directly as an Int16Array;\n // copy wrap-aware in at most two runs with no per-sample DataView call\n // or modulo. This runs on the realtime audio thread.\n const int16 = new Int16Array(bytes.buffer, bytes.byteOffset, numSamples);\n let src = 0;\n let dst = this.writePos % cap;\n while (src < numSamples) {\n const run = Math.min(numSamples - src, cap - dst);\n for (let j = 0; j < run; j++) {\n samples[dst + j] = int16[src + j] / 0x8000;\n }\n src += run;\n dst = 0;\n }\n } else {\n // Odd byte offset (or big-endian host): fall back to per-sample reads.\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);\n let dst = this.writePos % cap;\n for (let i = 0; i < numSamples; i++) {\n samples[dst] = view.getInt16(i * 2, true) / 0x8000;\n dst++;\n if (dst === cap) dst = 0;\n }\n }\n this.writePos += numSamples;\n // If the producer outran the consumer by more than the buffer holds, drop\n // the oldest unplayed audio rather than reading samples we've overwritten.\n if (this.writePos - this.readPos > this.capacity) {\n this.readPos = this.writePos - this.capacity;\n }\n }\n\n process(inputs, outputs) {\n // No output wired up yet \u2014 nothing to render this quantum. Throwing here\n // would permanently kill the processor (the node is persistent per\n // session), so guard like the capture processor does.\n if (!outputs[0] || !outputs[0][0]) return true;\n const out = outputs[0][0];\n const avail = this.writePos - this.readPos;\n\n // Report the unplayed backlog to the host (`playback_progress`). This IS\n // the closed-loop signal: the host otherwise models playback open-loop \u2014\n // every forwarded chunk assumed to start playing on arrival at exactly\n // 1.0x \u2014 and cannot see a buffer that has run ahead of the wall clock.\n // Measured against a client draining at 0.6x, the host declared the line\n // silent while it still held seconds of the reply, and then opened the\n // speaking-edge gate over speech the caller had not heard.\n //\n // Sent from process() rather than a timer because this is the only place\n // that sees the buffer on the audio thread; the counter is in QUANTA, so\n // it costs an integer compare per render and no clock read. Silence is not\n // reported: an empty buffer is what the host already assumes.\n this.sinceReportSamples += out.length;\n if (this.sinceReportSamples >= this.reportIntervalSamples) {\n this.sinceReportSamples = 0;\n if (avail > 0) {\n this.port.postMessage({ event: 'progress', bufferedMs: (avail / this.rate) * 1000 });\n }\n }\n\n // Filling: wait for the target. 'done' short-circuits it \u2014 what is\n // buffered is all there will be, so there is nothing left to wait for.\n if (!this.playing) {\n if (avail >= this.fillSamples || this.isDone) {\n this.playing = true;\n } else {\n this.coverGap(out, 0);\n return true;\n }\n }\n\n // Underrun: this quantum cannot be filled and more audio is still coming.\n // Go back to filling and cover the gap, leaving\n // readPos untouched \u2014 the fragment stays buffered and plays intact once\n // the buffer recovers, instead of being dribbled out a few samples at a\n // time for the rest of the turn.\n if (avail < out.length && !this.isDone) {\n this.playing = false;\n this.coverGap(out, 0);\n return true;\n }\n\n if (avail > 0) {\n const n = Math.min(avail, out.length);\n // Copy from the ring buffer, splitting across the wrap boundary.\n const start = this.readPos % this.capacity;\n const first = Math.min(n, this.capacity - start);\n out.set(this.samples.subarray(start, start + first), 0);\n if (n > first) out.set(this.samples.subarray(0, n - first), first);\n this.readPos += n;\n // Only reachable with n < out.length on the turn's final partial\n // quantum (the underrun branch above catches every other case).\n out.fill(0, n);\n this.hasPlayed = true;\n this.rememberTail(out, n);\n return true;\n }\n\n // Drained and done: end the turn. Not reachable mid-turn \u2014 an empty\n // buffer with audio still coming is the underrun branch above.\n out.fill(0);\n if (this.isDone) {\n this.stopTurn('done');\n }\n return true;\n }\n}\n\nregisterProcessor('playback-processor', PlaybackProcessor);\n";
3
+ declare const _default: string;
4
+ export default _default;
@@ -1,37 +1,190 @@
1
- //#region worklets/playback-processor.ts
2
- const script = new Blob([`
1
+ import { PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "../types.js";
2
+ import { t as workletModuleUrl } from "../_module-url-C13kAJ87.js";
3
+ //#region src/worklets/playback-processor.ts
4
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
5
+ const playbackProcessorSource = `
3
6
  class PlaybackProcessor extends AudioWorkletProcessor {
4
7
  constructor(options) {
5
8
  super();
6
- this.interrupted = false;
7
- this.isDone = false;
8
- this.playing = false;
9
- const rate = options.processorOptions?.sampleRate ?? 24000;
10
- // Wait for ~400ms of audio before starting.
11
- // If 'done' arrives first (short utterance), start immediately.
12
- this.jitterSamples = Math.floor(rate * 0.4);
13
- // Carry-over byte for split samples across chunks
14
- this.carry = null;
15
- // Float32 sample buffer — 60s at the context sample rate
16
- this.samples = new Float32Array(rate * 60);
17
- this.writePos = 0;
18
- this.readPos = 0;
19
- // Report playback position every ~50ms for word-level text sync
20
- this.progressInterval = Math.floor(rate * 0.05);
21
- this.samplesSinceProgress = 0;
9
+ const opts = options.processorOptions || {};
10
+ // The node's context IS the playback context (audio/voice-io.ts asserts its rate),
11
+ // so the worklet-global sampleRate is authoritative; the option exists
12
+ // for the node-less test harness.
13
+ const rate = opts.sampleRate ?? sampleRate;
14
+ // Kept, because every derived quantity below reads it and \`bufferedMs\` in
15
+ // process() is the one that used to read the worklet global instead. Those
16
+ // agree in production (the option is unset, so \`rate\` IS \`sampleRate\`) and
17
+ // diverge only in the node-less harness — which is the one place a spec
18
+ // could ever assert on the reported backlog, so the divergence made the
19
+ // number untestable rather than wrong.
20
+ this.rate = rate;
21
+ // The one fill target — for the start of a turn and for recovery after an
22
+ // underrun alike. If 'done' arrives first (short utterance), start
23
+ // immediately instead of waiting for audio that is never coming.
24
+ this.fillSamples = Math.floor((rate * (opts.fillMs ?? ${PLAYBACK_FILL_MS})) / 1000);
25
+ // Concealment source: a ring of the most recently played samples, looped
26
+ // under a decaying gain to cover a gap. Sized to the fade window, with a
27
+ // per-sample decay that reaches the floor exactly at its end.
28
+ this.concealCapacity = Math.max(1, Math.floor((rate * ${PLAYBACK_CONCEAL_FADE_MS}) / 1000));
29
+ this.concealBuf = new Float32Array(this.concealCapacity);
30
+ this.concealDecay = Math.exp(Math.log(${PLAYBACK_CONCEAL_FLOOR}) / this.concealCapacity);
31
+ // Float32 ring buffer — PLAYBACK_BUFFER_SECONDS at the context sample
32
+ // rate. Allocated once for the node's lifetime; per-turn state resets via
33
+ // resetTurn(). writePos and readPos are absolute (monotonic) sample
34
+ // counts; the buffer is indexed modulo capacity so a longer reply keeps
35
+ // playing instead of writing past the end and going silent.
36
+ this.capacity = rate * ${PLAYBACK_BUFFER_SECONDS};
37
+ this.samples = new Float32Array(this.capacity);
38
+ // Cadence for the 'progress' report in process(), counted in samples so
39
+ // the hot path never reads a clock. Survives resetTurn(): the host clamps
40
+ // upward only, so a stale count costs at most one extra report.
41
+ this.reportIntervalSamples = Math.floor((rate * ${PLAYBACK_PROGRESS_INTERVAL_MS}) / 1000);
42
+ this.sinceReportSamples = 0;
43
+ // Platform endianness probe: the wire format is PCM16 little-endian, so
44
+ // the Int16Array fast path in ingestBytes is only valid on LE hosts
45
+ // (every shipping browser target; the DataView path is the fallback).
46
+ this.littleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
47
+ this.resetTurn();
22
48
 
23
49
  this.port.onmessage = (e) => {
24
50
  const d = e.data;
25
51
  if (d.event === 'write') {
26
52
  this.ingestBytes(d.buffer);
27
53
  } else if (d.event === 'interrupt') {
28
- this.interrupted = true;
54
+ // Applied eagerly, not deferred to the next process(): onmessage and
55
+ // process() never interleave (one audio thread), and a deferred
56
+ // interrupt would let 'write'/'done' frames for the NEXT turn
57
+ // coalesce in ahead of it and be wiped by the reset along with the
58
+ // cancelled turn's audio.
59
+ this.stopTurn('interrupt');
29
60
  } else if (d.event === 'done') {
30
61
  this.isDone = true;
62
+ // Echoed back on this turn's 'stop' so the host can tell WHICH turn
63
+ // drained: a stop already in flight when a barge-in lands would
64
+ // otherwise settle the next turn's done() the moment it arrives.
65
+ this.doneTurn = d.turn ?? null;
31
66
  }
32
67
  };
33
68
  }
34
69
 
70
+ // Reset per-turn state so the node is reusable across replies without
71
+ // reallocating the sample buffer or re-instantiating the worklet.
72
+ resetTurn() {
73
+ this.isDone = false;
74
+ // Host turn id carried by this turn's 'done' (null until one arrives).
75
+ this.doneTurn = null;
76
+ this.playing = false;
77
+ // Whether any real audio has been rendered this turn. Separates a turn's
78
+ // pre-roll (nothing to extrapolate from, and not a defect) from a
79
+ // mid-turn underrun.
80
+ this.hasPlayed = false;
81
+ // Carry-over byte for split samples across chunks
82
+ this.carry = null;
83
+ this.writePos = 0;
84
+ this.readPos = 0;
85
+ // Concealment ring state and the current fade position.
86
+ this.concealLen = 0;
87
+ this.concealWrite = 0;
88
+ this.concealPos = 0;
89
+ this.concealGain = 1;
90
+ // Episode flags, so a multi-quantum gap counts as one event.
91
+ this.concealing = false;
92
+ this.concealedSilence = false;
93
+ // Reported to the host on 'stop'. A fresh object per turn: the one just
94
+ // posted must not be mutated by the next turn.
95
+ this.stats = {
96
+ concealedSamples: 0,
97
+ silentConcealedSamples: 0,
98
+ concealmentEvents: 0,
99
+ silentConcealmentEvents: 0,
100
+ };
101
+ }
102
+
103
+ // End the current turn: notify the host and rearm for the next reply.
104
+ // Must NOT return false from process() — a processor that stops is dead
105
+ // for good, forcing a new node (and buffer) per reply.
106
+ // \`reason\` ('interrupt' | 'done') tells the host which turn boundary this
107
+ // stop belongs to — interrupt-stops are dropped host-side, flush() having
108
+ // already settled that turn — and \`turn\` names WHICH turn drained (the id
109
+ // this turn's 'done' carried), so a drain-stop still in flight when a
110
+ // barge-in lands cannot settle the next turn's done() early.
111
+ stopTurn(reason) {
112
+ this.port.postMessage({ event: 'stop', reason, turn: this.doneTurn, stats: this.stats });
113
+ this.resetTurn();
114
+ }
115
+
116
+ // Cover a quantum (from \`start\`) where real audio should have been.
117
+ //
118
+ // Before the turn's first samples there is nothing to extrapolate from, so
119
+ // the gap is plain silence and counted as nothing — WebRTC likewise only
120
+ // counts concealment once playout has begun. After that, loop the retained
121
+ // tail under a decaying gain: a hard zero-fill is a discontinuity mid-word,
122
+ // which is the click that makes a brief stall sound like breakage.
123
+ coverGap(out, start) {
124
+ if (!this.hasPlayed) {
125
+ out.fill(0, start);
126
+ return;
127
+ }
128
+ if (!this.concealing) {
129
+ this.concealing = true;
130
+ this.concealedSilence = false;
131
+ this.stats.concealmentEvents++;
132
+ }
133
+ const total = out.length - start;
134
+ const len = this.concealLen;
135
+ let silent = 0;
136
+ // Second condition: once the fade has decayed to the floor, every sample
137
+ // the loop would emit is 0 anyway — bulk-fill instead of running 128
138
+ // branchy iterations per quantum for the whole tail of a long stall.
139
+ if (len === 0 || this.concealGain < ${PLAYBACK_CONCEAL_FLOOR}) {
140
+ out.fill(0, start);
141
+ silent = total;
142
+ } else {
143
+ let g = this.concealGain;
144
+ for (let i = start; i < out.length; i++) {
145
+ if (g < ${PLAYBACK_CONCEAL_FLOOR}) {
146
+ // The fade has run out: keep counting the gap, but stop looping a
147
+ // fragment that is now inaudible anyway.
148
+ out[i] = 0;
149
+ silent++;
150
+ continue;
151
+ }
152
+ out[i] = this.concealBuf[this.concealPos] * g;
153
+ this.concealPos = this.concealPos + 1 === len ? 0 : this.concealPos + 1;
154
+ g *= this.concealDecay;
155
+ }
156
+ this.concealGain = g;
157
+ }
158
+ this.stats.concealedSamples += total;
159
+ if (silent > 0) {
160
+ this.stats.silentConcealedSamples += silent;
161
+ if (!this.concealedSilence) {
162
+ this.concealedSilence = true;
163
+ this.stats.silentConcealmentEvents++;
164
+ }
165
+ }
166
+ }
167
+
168
+ // Retain the tail of a rendered quantum as the next gap's concealment
169
+ // source, and close any episode the real audio just ended.
170
+ rememberTail(out, n) {
171
+ const cap = this.concealCapacity;
172
+ const take = Math.min(n, cap);
173
+ // Bulk copies (this runs on every cleanly rendered quantum): the tail is
174
+ // one contiguous source run, landing in at most two ring runs.
175
+ const tail = out.subarray(n - take, n);
176
+ const first = Math.min(take, cap - this.concealWrite);
177
+ this.concealBuf.set(tail.subarray(0, first), this.concealWrite);
178
+ if (take > first) this.concealBuf.set(tail.subarray(first), 0);
179
+ this.concealWrite = (this.concealWrite + take) % cap;
180
+ this.concealLen = Math.min(cap, this.concealLen + take);
181
+ // Read the loop oldest-first; once the ring is full the write cursor is
182
+ // the oldest retained sample.
183
+ this.concealPos = this.concealLen === cap ? this.concealWrite : 0;
184
+ this.concealing = false;
185
+ this.concealGain = 1;
186
+ }
187
+
35
188
  ingestBytes(uint8) {
36
189
  let bytes = uint8;
37
190
 
@@ -49,57 +202,120 @@ class PlaybackProcessor extends AudioWorkletProcessor {
49
202
  }
50
203
 
51
204
  if (bytes.length === 0) return;
52
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
53
205
  const numSamples = bytes.length / 2;
54
- for (let i = 0; i < numSamples; i++) {
55
- this.samples[this.writePos++] = view.getInt16(i * 2, true) / 0x8000;
206
+ const cap = this.capacity;
207
+ const samples = this.samples;
208
+ if (this.littleEndian && (bytes.byteOffset & 1) === 0) {
209
+ // Fast path: 2-byte-aligned LE bytes wrap directly as an Int16Array;
210
+ // copy wrap-aware in at most two runs with no per-sample DataView call
211
+ // or modulo. This runs on the realtime audio thread.
212
+ const int16 = new Int16Array(bytes.buffer, bytes.byteOffset, numSamples);
213
+ let src = 0;
214
+ let dst = this.writePos % cap;
215
+ while (src < numSamples) {
216
+ const run = Math.min(numSamples - src, cap - dst);
217
+ for (let j = 0; j < run; j++) {
218
+ samples[dst + j] = int16[src + j] / 0x8000;
219
+ }
220
+ src += run;
221
+ dst = 0;
222
+ }
223
+ } else {
224
+ // Odd byte offset (or big-endian host): fall back to per-sample reads.
225
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
226
+ let dst = this.writePos % cap;
227
+ for (let i = 0; i < numSamples; i++) {
228
+ samples[dst] = view.getInt16(i * 2, true) / 0x8000;
229
+ dst++;
230
+ if (dst === cap) dst = 0;
231
+ }
232
+ }
233
+ this.writePos += numSamples;
234
+ // If the producer outran the consumer by more than the buffer holds, drop
235
+ // the oldest unplayed audio rather than reading samples we've overwritten.
236
+ if (this.writePos - this.readPos > this.capacity) {
237
+ this.readPos = this.writePos - this.capacity;
56
238
  }
57
239
  }
58
240
 
59
241
  process(inputs, outputs) {
242
+ // No output wired up yet — nothing to render this quantum. Throwing here
243
+ // would permanently kill the processor (the node is persistent per
244
+ // session), so guard like the capture processor does.
245
+ if (!outputs[0] || !outputs[0][0]) return true;
60
246
  const out = outputs[0][0];
61
- if (this.interrupted) {
62
- this.port.postMessage({ event: 'stop' });
63
- return false;
64
- }
65
-
66
247
  const avail = this.writePos - this.readPos;
67
248
 
68
- // Wait for jitter buffer to fill, unless done (short utterance)
249
+ // Report the unplayed backlog to the host (\`playback_progress\`). This IS
250
+ // the closed-loop signal: the host otherwise models playback open-loop —
251
+ // every forwarded chunk assumed to start playing on arrival at exactly
252
+ // 1.0x — and cannot see a buffer that has run ahead of the wall clock.
253
+ // Measured against a client draining at 0.6x, the host declared the line
254
+ // silent while it still held seconds of the reply, and then opened the
255
+ // speaking-edge gate over speech the caller had not heard.
256
+ //
257
+ // Sent from process() rather than a timer because this is the only place
258
+ // that sees the buffer on the audio thread; the counter is in QUANTA, so
259
+ // it costs an integer compare per render and no clock read. Silence is not
260
+ // reported: an empty buffer is what the host already assumes.
261
+ this.sinceReportSamples += out.length;
262
+ if (this.sinceReportSamples >= this.reportIntervalSamples) {
263
+ this.sinceReportSamples = 0;
264
+ if (avail > 0) {
265
+ this.port.postMessage({ event: 'progress', bufferedMs: (avail / this.rate) * 1000 });
266
+ }
267
+ }
268
+
269
+ // Filling: wait for the target. 'done' short-circuits it — what is
270
+ // buffered is all there will be, so there is nothing left to wait for.
69
271
  if (!this.playing) {
70
- if (avail >= this.jitterSamples || this.isDone) {
272
+ if (avail >= this.fillSamples || this.isDone) {
71
273
  this.playing = true;
72
274
  } else {
73
- out.fill(0);
275
+ this.coverGap(out, 0);
74
276
  return true;
75
277
  }
76
278
  }
77
279
 
280
+ // Underrun: this quantum cannot be filled and more audio is still coming.
281
+ // Go back to filling and cover the gap, leaving
282
+ // readPos untouched — the fragment stays buffered and plays intact once
283
+ // the buffer recovers, instead of being dribbled out a few samples at a
284
+ // time for the rest of the turn.
285
+ if (avail < out.length && !this.isDone) {
286
+ this.playing = false;
287
+ this.coverGap(out, 0);
288
+ return true;
289
+ }
290
+
78
291
  if (avail > 0) {
79
292
  const n = Math.min(avail, out.length);
80
- out.set(this.samples.subarray(this.readPos, this.readPos + n));
293
+ // Copy from the ring buffer, splitting across the wrap boundary.
294
+ const start = this.readPos % this.capacity;
295
+ const first = Math.min(n, this.capacity - start);
296
+ out.set(this.samples.subarray(start, start + first), 0);
297
+ if (n > first) out.set(this.samples.subarray(0, n - first), first);
81
298
  this.readPos += n;
82
- this.samplesSinceProgress += n;
83
- if (this.samplesSinceProgress >= this.progressInterval) {
84
- this.port.postMessage({ event: 'progress', readPos: this.readPos });
85
- this.samplesSinceProgress = 0;
86
- }
299
+ // Only reachable with n < out.length on the turn's final partial
300
+ // quantum (the underrun branch above catches every other case).
87
301
  out.fill(0, n);
302
+ this.hasPlayed = true;
303
+ this.rememberTail(out, n);
88
304
  return true;
89
305
  }
90
306
 
91
- // No data: output silence, stop only when done
307
+ // Drained and done: end the turn. Not reachable mid-turn — an empty
308
+ // buffer with audio still coming is the underrun branch above.
92
309
  out.fill(0);
93
310
  if (this.isDone) {
94
- this.port.postMessage({ event: 'stop' });
95
- return false;
311
+ this.stopTurn('done');
96
312
  }
97
313
  return true;
98
314
  }
99
315
  }
100
316
 
101
317
  registerProcessor('playback-processor', PlaybackProcessor);
102
- `], { type: "application/javascript" });
103
- const src = URL.createObjectURL(script);
318
+ `;
319
+ var playback_processor_default = workletModuleUrl(playbackProcessorSource);
104
320
  //#endregion
105
- export { src as default };
321
+ export { playback_processor_default as default, playbackProcessorSource };
package/package.json CHANGED
@@ -1,59 +1,86 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/alexkroman/agent.git",
9
+ "directory": "packages/aai-ui"
10
+ },
11
+ "engines": {
12
+ "node": ">=24"
13
+ },
14
+ "sideEffects": [
15
+ "*.css",
16
+ "**/*.css"
17
+ ],
5
18
  "files": [
6
19
  "dist",
7
20
  "styles.css"
8
21
  ],
9
22
  "exports": {
10
23
  ".": {
11
- "@dev/source": "./index.ts",
24
+ "@dev/source": "./src/index.ts",
12
25
  "types": "./dist/index.d.ts",
13
26
  "import": "./dist/index.js"
14
27
  },
28
+ "./client-dir": {
29
+ "@dev/source": "./src/client-dir.ts",
30
+ "types": "./dist/client-dir.d.ts",
31
+ "import": "./dist/client-dir.js"
32
+ },
33
+ "./internal": {
34
+ "@dev/source": "./src/internal.ts",
35
+ "types": "./dist/internal.d.ts",
36
+ "import": "./dist/internal.js"
37
+ },
15
38
  "./styles.css": "./styles.css",
16
- "./session": {
17
- "@dev/source": "./session.ts",
18
- "types": "./dist/session.d.ts",
19
- "import": "./dist/session.js"
20
- }
39
+ "./package.json": "./package.json"
21
40
  },
22
41
  "dependencies": {
23
42
  "clsx": "^2.1.1",
24
- "@alexkroman1/aai": "0.12.2"
43
+ "partysocket": "^1.3.0",
44
+ "react-markdown": "^10.1.0",
45
+ "remark-gfm": "^4.0.1",
46
+ "use-stick-to-bottom": "^1.1.6",
47
+ "use-sync-external-store": "^1.7.0",
48
+ "xstate": "^5.33.2",
49
+ "@alexkroman1/aai": "0.13.0"
50
+ },
51
+ "devDependencies": {
52
+ "@tailwindcss/vite": "^4.3.3",
53
+ "@testing-library/dom": "^10.4.2",
54
+ "@testing-library/react": "^16.3.3",
55
+ "@types/react": "^19.3.0",
56
+ "@types/react-dom": "^19.3.0",
57
+ "@types/use-sync-external-store": "^1.7.0",
58
+ "@vitejs/plugin-react": "^6.1.1",
59
+ "fast-check": "^4.10.2",
60
+ "jsdom": "~30.0.1",
61
+ "react": "^19.3.0",
62
+ "react-dom": "^19.3.0",
63
+ "tsdown": "^0.22.14",
64
+ "vite": "^8.3.1",
65
+ "vitest": "^5.0.1"
25
66
  },
26
67
  "peerDependencies": {
27
- "@preact/signals": "^2.8.2",
28
- "preact": "^10.29.0",
29
- "tailwindcss": "^4.2.1"
68
+ "react": "^19.0.0",
69
+ "react-dom": "^19.0.0",
70
+ "tailwindcss": "^4.0.0"
30
71
  },
31
72
  "peerDependenciesMeta": {
32
73
  "tailwindcss": {
33
74
  "optional": true
34
75
  }
35
76
  },
36
- "devDependencies": {
37
- "@preact/signals": "^2.8.2",
38
- "@testing-library/preact": "^3.2.4",
39
- "jsdom": "^29.0.1",
40
- "preact": "^10.29.0",
41
- "tsdown": "^0.21.5",
42
- "vitest": "^4.1.1"
43
- },
44
- "engines": {
45
- "node": ">=22.6"
46
- },
47
- "repository": {
48
- "type": "git",
49
- "url": "https://github.com/alexkroman/agent.git",
50
- "directory": "packages/aai-ui"
51
- },
52
77
  "scripts": {
53
- "build": "tsdown && tsc -p tsconfig.build.json",
54
- "typecheck": "tsc --noEmit",
55
- "lint": "biome check .",
78
+ "build": "tsdown && tsc -p tsconfig.build.json && node src/build-default-client.ts",
79
+ "check:attw": "attw --pack --profile esm-only --exclude-entrypoints styles.css",
56
80
  "check:publint": "publint",
57
- "check:attw": "attw --pack --profile esm-only --entrypoints . ./session"
81
+ "lint": "biome check .",
82
+ "test": "vitest run",
83
+ "test:coverage": "vitest run --coverage",
84
+ "typecheck": "tsc --noEmit"
58
85
  }
59
86
  }