cursor-buddy 0.0.9-beta.0 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ export const cursorBuddy = createCursorBuddyHandler({
57
57
  import { toNextJsHandler } from "cursor-buddy/server/next"
58
58
  import { cursorBuddy } from "@/lib/cursor-buddy"
59
59
 
60
- export const { GET, POST } = toNextJsHandler(cursorBuddy)
60
+ export const { POST } = toNextJsHandler(cursorBuddy)
61
61
  ```
62
62
 
63
63
  ### 2. Client Setup
@@ -371,13 +371,13 @@ client.stopListening()
371
371
  4. An annotated screenshot of the viewport is captured, with numbered markers on visible interactive elements, based on [agent-browser](https://github.com/vercel-labs/agent-browser) implementation.
372
372
  5. The client prefers the browser transcript; if it is unavailable or empty in `auto` mode, the recorded audio is transcribed on the server
373
373
  6. Screenshot + marker context are sent to the AI model
374
- 7. AI responds with text, optionally including a pointing tag:
375
- - Preferred: `[POINT:5:Submit]` for numbered interactive elements
376
- - Fallback: `[POINT:640,360:Error text]` for arbitrary screen coordinates
374
+ 7. AI responds with text and can optionally call the `point` tool to indicate a location on screen:
375
+ - `type: "marker"` with `markerId` for numbered interactive elements (most accurate)
376
+ - `type: "coordinates"` with `x, y` pixel coordinates for anything without a marker
377
377
  8. Response is spoken in the browser or on the server based on `speech.mode`,
378
378
  and can either wait for the full response or stream sentence-by-sentence
379
379
  based on `speech.allowStreaming`
380
- 9. If a marker tag is present, it is resolved back to the live DOM element; if a coordinate tag is present, it is mapped back to the live viewport; then the cursor animates to the target location
380
+ 9. If the AI calls the point tool, the cursor animates to the target location markers resolve to live DOM elements, coordinates map to viewport positions
381
381
  10. **If user presses hotkey again at any point, current response is interrupted**
382
382
 
383
383
  ## Security Best Practices
@@ -415,7 +415,6 @@ export const GET = POST
415
415
 
416
416
  ## TODOs
417
417
 
418
- - [ ] High: Make tool calls first class: Pointing becomes tool call (once per turn) + re-use pointing bubble UI for tool calls
419
418
  - [ ] Medium: Proper test structure without relying on `as any` for audio and voice capture
420
419
 
421
420
  ## License
@@ -617,7 +617,7 @@ const DEFAULT_STYLE = {
617
617
  labelBackground: "rgba(255, 0, 0, 0.9)",
618
618
  labelColor: "#ffffff",
619
619
  borderWidth: 2,
620
- fontSize: 11,
620
+ fontSize: 15,
621
621
  labelPadding: 4
622
622
  };
623
623
  /**
@@ -935,7 +935,7 @@ async function waitForClonedDocumentStyles(doc) {
935
935
  }
936
936
  function getHtml2CanvasOptions(captureMetrics) {
937
937
  return {
938
- scale: 1,
938
+ scale: window.devicePixelRatio,
939
939
  useCORS: true,
940
940
  logging: false,
941
941
  width: captureMetrics.viewportWidth,
@@ -2251,4 +2251,4 @@ var CursorBuddyClient = class {
2251
2251
  //#endregion
2252
2252
  export { $buddyScale as a, $buddyRotation as i, $audioLevel as n, $cursorPosition as o, $buddyPosition as r, $pointingTarget as s, CursorBuddyClient as t };
2253
2253
 
2254
- //# sourceMappingURL=client-CevxN9EX.mjs.map
2254
+ //# sourceMappingURL=client-CSVSY-KV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-CSVSY-KV.mjs","names":["clamp"],"sources":["../src/core/atoms.ts","../src/core/utils/error.ts","../src/core/services/audio-playback.ts","../src/core/utils/web-speech.ts","../src/core/services/browser-speech.ts","../src/core/services/live-transcription.ts","../src/core/bezier.ts","../src/core/services/pointer-controller.ts","../src/core/utils/annotations.ts","../src/core/utils/elements.ts","../src/core/utils/screenshot.ts","../src/core/services/screen-capture.ts","../src/core/services/tts-playback-queue.ts","../src/core/utils/audio.ts","../src/core/utils/audio-worklet.ts","../src/core/services/voice-capture.ts","../src/core/state-machine.ts","../src/core/utils/ui-stream-parser.ts","../src/core/utils/response-processor.ts","../src/core/client.ts"],"sourcesContent":["import { atom } from \"nanostores\"\nimport type { ConversationMessage, Point, PointingTarget } from \"./types\"\n\n/**\n * Nanostores atoms for reactive values that don't need state machine semantics.\n * These update frequently (e.g., 60fps audio levels) and are framework-agnostic.\n */\n\n// Audio level during recording (0-1, updates at ~60fps)\nexport const $audioLevel = atom<number>(0)\n\n// Mouse cursor position (real cursor, not buddy)\nexport const $cursorPosition = atom<Point>({ x: 0, y: 0 })\n\n// Buddy animated position (follows cursor with spring physics, or flies to target)\nexport const $buddyPosition = atom<Point>({ x: 0, y: 0 })\n\n// Buddy rotation in radians (direction of travel during pointing)\nexport const $buddyRotation = atom<number>(0)\n\n// Buddy scale (1.0 normal, up to 1.3 during flight)\nexport const $buddyScale = atom<number>(1)\n\n// Current pointing target parsed from AI response\nexport const $pointingTarget = atom<PointingTarget | null>(null)\n\n// Whether buddy overlay is enabled/visible\nexport const $isEnabled = atom<boolean>(true)\n\n// Whether TTS is currently playing\nexport const $isSpeaking = atom<boolean>(false)\n\n// Conversation history for context\nexport const $conversationHistory = atom<ConversationMessage[]>([])\n","/**\n * Normalize unknown thrown values into Error instances.\n */\nexport function toError(\n error: unknown,\n fallbackMessage: string = \"Unknown error\",\n): Error {\n if (error instanceof Error) {\n return error\n }\n\n if (typeof error === \"string\" && error) {\n return new Error(error)\n }\n\n return new Error(fallbackMessage)\n}\n","import type { AudioPlaybackPort } from \"../types\"\nimport { toError } from \"../utils/error\"\n\n/**\n * Framework-agnostic service for audio playback with abort support.\n */\nexport class AudioPlaybackService implements AudioPlaybackPort {\n private audio: HTMLAudioElement | null = null\n private currentUrl: string | null = null\n private settlePlayback:\n | ((outcome: \"resolve\" | \"reject\", error?: Error) => void)\n | null = null\n private removeAbortListener: (() => void) | null = null\n\n /**\n * Play audio from a blob. Stops any currently playing audio first.\n * @param blob - Audio blob to play\n * @param signal - Optional AbortSignal to cancel playback\n * @returns Promise that resolves when playback completes\n */\n async play(blob: Blob, signal?: AbortSignal): Promise<void> {\n // Stop any current playback\n this.stop()\n\n // Check if already aborted\n if (signal?.aborted) return\n\n const url = URL.createObjectURL(blob)\n this.currentUrl = url\n this.audio = new Audio(url)\n\n return new Promise<void>((resolve, reject) => {\n if (!this.audio) {\n this.cleanup()\n resolve()\n return\n }\n\n let settled = false\n const audio = this.audio\n\n const settle = (outcome: \"resolve\" | \"reject\", error?: Error) => {\n if (settled) return\n settled = true\n\n if (this.settlePlayback === settle) {\n this.settlePlayback = null\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n\n if (this.audio === audio) {\n this.audio.onended = null\n this.audio.onerror = null\n this.audio = null\n }\n\n this.cleanup()\n\n if (outcome === \"resolve\") {\n resolve()\n return\n }\n\n reject(error ?? new Error(\"Audio playback failed\"))\n }\n\n this.settlePlayback = settle\n\n const abortHandler = () => {\n audio.pause()\n settle(\"resolve\")\n }\n\n if (signal) {\n signal.addEventListener(\"abort\", abortHandler, { once: true })\n this.removeAbortListener = () => {\n signal.removeEventListener(\"abort\", abortHandler)\n }\n }\n\n this.audio.onended = () => {\n settle(\"resolve\")\n }\n\n this.audio.onerror = () => {\n settle(\"reject\", new Error(\"Audio playback failed\"))\n }\n\n this.audio.play().catch((err) => {\n settle(\"reject\", toError(err, \"Audio playback failed\"))\n })\n })\n }\n\n /**\n * Stop any currently playing audio.\n */\n stop(): void {\n if (this.audio) {\n this.audio.pause()\n }\n\n if (this.settlePlayback) {\n const settlePlayback = this.settlePlayback\n this.settlePlayback = null\n settlePlayback(\"resolve\")\n return\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n\n if (this.audio) {\n this.audio.onended = null\n this.audio.onerror = null\n this.audio = null\n }\n\n this.cleanup()\n }\n\n private cleanup(): void {\n if (this.currentUrl) {\n URL.revokeObjectURL(this.currentUrl)\n this.currentUrl = null\n }\n }\n}\n","/**\n * Normalize browser speech input and transcript output to a single-space form\n * so UI state and speech synthesis stay stable across browser event quirks.\n */\nexport function normalizeSpeechText(text: string): string {\n return text.replace(/\\s+/g, \" \").trim()\n}\n\n/**\n * Resolve the best browser locale to use for Web Speech APIs.\n *\n * We prefer the document language when the host app declares one, then fall\n * back to the browser locale, and finally to English as a stable default.\n */\nexport function resolveBrowserLanguage(): string {\n if (typeof document !== \"undefined\") {\n const documentLanguage = document.documentElement.lang.trim()\n if (documentLanguage) return documentLanguage\n }\n\n if (typeof navigator !== \"undefined\" && navigator.language) {\n return navigator.language\n }\n\n return \"en-US\"\n}\n","import type { BrowserSpeechPort } from \"../types\"\nimport { toError } from \"../utils/error\"\nimport {\n normalizeSpeechText,\n resolveBrowserLanguage,\n} from \"../utils/web-speech\"\n\nfunction getSpeechSynthesis(): SpeechSynthesis | undefined {\n return typeof globalThis.speechSynthesis === \"undefined\"\n ? undefined\n : globalThis.speechSynthesis\n}\n\nfunction getSpeechSynthesisUtterance():\n | typeof SpeechSynthesisUtterance\n | undefined {\n return typeof globalThis.SpeechSynthesisUtterance === \"undefined\"\n ? undefined\n : globalThis.SpeechSynthesisUtterance\n}\n\nfunction toSpeechError(event?: SpeechSynthesisErrorEvent): Error {\n const errorCode = event?.error\n\n return new Error(\n errorCode ? `Browser speech failed: ${errorCode}` : \"Browser speech failed\",\n )\n}\n\n/**\n * Browser-backed speech synthesis using the Web Speech API.\n */\nexport class BrowserSpeechService implements BrowserSpeechPort {\n private removeAbortListener: (() => void) | null = null\n private settleSpeech:\n | ((outcome: \"resolve\" | \"reject\", error?: Error) => void)\n | null = null\n private utterance: SpeechSynthesisUtterance | null = null\n\n /**\n * Report whether this runtime exposes the browser Web Speech synthesis APIs.\n */\n isAvailable(): boolean {\n return Boolean(getSpeechSynthesis() && getSpeechSynthesisUtterance())\n }\n\n /**\n * Speak a single text segment in the browser.\n *\n * Each queue item owns its own utterance. We only stop an existing utterance\n * when this service still has one in flight, so streamed playback does not\n * spam global `speechSynthesis.cancel()` between already-completed segments.\n */\n async speak(text: string, signal?: AbortSignal): Promise<void> {\n const speechSynthesis = getSpeechSynthesis()\n const SpeechSynthesisUtteranceCtor = getSpeechSynthesisUtterance()\n\n if (!speechSynthesis || !SpeechSynthesisUtteranceCtor) {\n throw new Error(\"Browser speech is not supported\")\n }\n\n if (this.hasActiveSpeech()) {\n this.stop()\n }\n\n const normalizedText = normalizeSpeechText(text)\n if (!normalizedText || signal?.aborted) return\n\n const utterance = new SpeechSynthesisUtteranceCtor(normalizedText)\n utterance.lang = resolveBrowserLanguage()\n this.utterance = utterance\n\n return new Promise<void>((resolve, reject) => {\n let settled = false\n\n const settle = (outcome: \"resolve\" | \"reject\", error?: Error) => {\n if (settled) return\n settled = true\n\n if (this.settleSpeech === settle) {\n this.settleSpeech = null\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n this.clearUtterance(utterance)\n\n if (outcome === \"resolve\") {\n resolve()\n return\n }\n\n reject(error ?? new Error(\"Browser speech failed\"))\n }\n\n this.settleSpeech = settle\n\n const abortHandler = () => {\n try {\n speechSynthesis.cancel()\n } catch {\n // Ignore cancel failures during abort cleanup.\n }\n\n settle(\"resolve\")\n }\n\n if (signal) {\n signal.addEventListener(\"abort\", abortHandler, { once: true })\n this.removeAbortListener = () => {\n signal.removeEventListener(\"abort\", abortHandler)\n }\n }\n\n utterance.onend = () => {\n settle(\"resolve\")\n }\n\n utterance.onerror = (event) => {\n if (signal?.aborted) {\n settle(\"resolve\")\n return\n }\n\n settle(\"reject\", toSpeechError(event))\n }\n\n try {\n speechSynthesis.speak(utterance)\n } catch (error) {\n settle(\"reject\", toError(error, \"Browser speech failed to start\"))\n }\n })\n }\n\n /**\n * Stop the current utterance owned by this service, if one is active.\n *\n * We intentionally do nothing when the service is idle so we do not cancel\n * unrelated speech synthesis work that host apps may be doing elsewhere.\n */\n stop(): void {\n if (!this.hasActiveSpeech()) {\n return\n }\n\n const speechSynthesis = getSpeechSynthesis()\n\n if (speechSynthesis) {\n try {\n speechSynthesis.cancel()\n } catch {\n // Ignore cancel failures during cleanup.\n }\n }\n\n if (this.settleSpeech) {\n const settleSpeech = this.settleSpeech\n this.settleSpeech = null\n settleSpeech(\"resolve\")\n return\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n this.clearUtterance(this.utterance)\n }\n\n private hasActiveSpeech(): boolean {\n return Boolean(this.utterance || this.settleSpeech)\n }\n\n private clearUtterance(utterance: SpeechSynthesisUtterance | null): void {\n if (!utterance) return\n\n utterance.onend = null\n utterance.onerror = null\n\n if (this.utterance === utterance) {\n this.utterance = null\n }\n }\n}\n","import type { LiveTranscriptionPort } from \"../types\"\nimport { toError } from \"../utils/error\"\nimport {\n normalizeSpeechText,\n resolveBrowserLanguage,\n} from \"../utils/web-speech\"\n\ninterface SpeechRecognitionAlternativeLike {\n transcript: string\n}\n\ninterface SpeechRecognitionResultLike {\n isFinal: boolean\n length: number\n [index: number]: SpeechRecognitionAlternativeLike\n}\n\ninterface SpeechRecognitionResultListLike {\n length: number\n [index: number]: SpeechRecognitionResultLike\n}\n\ninterface SpeechRecognitionEventLike {\n results: SpeechRecognitionResultListLike\n}\n\ninterface SpeechRecognitionErrorEventLike {\n error?: string\n message?: string\n}\n\ninterface SpeechRecognitionLike {\n continuous: boolean\n interimResults: boolean\n lang: string\n maxAlternatives: number\n onend: (() => void) | null\n onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null\n onresult: ((event: SpeechRecognitionEventLike) => void) | null\n onstart: (() => void) | null\n abort(): void\n start(): void\n stop(): void\n}\n\ntype SpeechRecognitionConstructor = new () => SpeechRecognitionLike\n\ntype GlobalWithSpeechRecognition = typeof globalThis & {\n SpeechRecognition?: SpeechRecognitionConstructor\n webkitSpeechRecognition?: SpeechRecognitionConstructor\n}\n\nfunction getSpeechRecognitionConstructor():\n | SpeechRecognitionConstructor\n | undefined {\n const globalScope = globalThis as GlobalWithSpeechRecognition\n\n return globalScope.SpeechRecognition ?? globalScope.webkitSpeechRecognition\n}\n\nfunction toRecognitionError(event?: SpeechRecognitionErrorEventLike): Error {\n const errorCode = event?.error\n const message =\n event?.message ||\n (errorCode\n ? `Browser transcription failed: ${errorCode}`\n : \"Browser transcription failed\")\n\n return new Error(message)\n}\n\nfunction buildTranscripts(results: SpeechRecognitionResultListLike): {\n finalTranscript: string\n liveTranscript: string\n} {\n let finalTranscript = \"\"\n let interimTranscript = \"\"\n\n // Web Speech returns the running recognition result list on every event.\n // We rebuild both views each time so the client always sees the freshest\n // \"confirmed + in-progress\" transcript.\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index]\n const alternative = result?.[0]\n const transcript = alternative?.transcript ?? \"\"\n\n if (!transcript) continue\n\n if (result.isFinal) {\n finalTranscript += `${transcript} `\n } else {\n interimTranscript += `${transcript} `\n }\n }\n\n const normalizedFinal = normalizeSpeechText(finalTranscript)\n const normalizedInterim = normalizeSpeechText(interimTranscript)\n\n return {\n finalTranscript: normalizedFinal,\n liveTranscript: normalizeSpeechText(\n [normalizedFinal, normalizedInterim].filter(Boolean).join(\" \"),\n ),\n }\n}\n\n/**\n * Browser-backed live transcription using the Web Speech API.\n */\nexport class LiveTranscriptionService implements LiveTranscriptionPort {\n private finalTranscript = \"\"\n private hasStarted = false\n private hasEnded = false\n private lastError: Error | null = null\n private partialCallback: ((text: string) => void) | null = null\n private recognition: SpeechRecognitionLike | null = null\n private startReject: ((reason?: unknown) => void) | null = null\n private startResolve: (() => void) | null = null\n private stopReject: ((reason?: unknown) => void) | null = null\n private stopResolve: ((value: string) => void) | null = null\n\n isAvailable(): boolean {\n return Boolean(getSpeechRecognitionConstructor())\n }\n\n /**\n * Register a callback for the latest browser transcript while the user is\n * still speaking.\n */\n onPartial(callback: (text: string) => void): void {\n this.partialCallback = callback\n }\n\n /**\n * Start a new Web Speech recognition session.\n */\n async start(): Promise<void> {\n const SpeechRecognitionCtor = getSpeechRecognitionConstructor()\n if (!SpeechRecognitionCtor) {\n throw new Error(\"Browser transcription is not supported\")\n }\n\n // Each push-to-talk turn owns a fresh recognition session. We clear any\n // previous session first so late events do not leak into the next turn.\n this.dispose()\n\n const recognition = new SpeechRecognitionCtor()\n this.recognition = recognition\n recognition.continuous = true\n recognition.interimResults = true\n recognition.maxAlternatives = 1\n recognition.lang = resolveBrowserLanguage()\n recognition.onstart = () => {\n this.hasStarted = true\n this.startResolve?.()\n this.startResolve = null\n this.startReject = null\n }\n recognition.onresult = (event) => {\n const transcripts = buildTranscripts(event.results)\n this.finalTranscript = transcripts.finalTranscript\n this.partialCallback?.(transcripts.liveTranscript)\n }\n recognition.onerror = (event) => {\n this.lastError = toRecognitionError(event)\n\n // Errors before `onstart` should reject startup immediately. Errors after\n // startup are handled when the session ends or when stop() awaits it.\n if (!this.hasStarted) {\n this.startReject?.(this.lastError)\n this.startResolve = null\n this.startReject = null\n }\n }\n recognition.onend = () => {\n this.hasEnded = true\n\n // Some browsers can jump straight to `end` when recognition is blocked\n // or cancelled before startup. Convert that into a startup failure.\n if (!this.hasStarted) {\n const error =\n this.lastError ??\n new Error(\"Browser transcription ended before it could start\")\n\n this.startReject?.(error)\n this.startResolve = null\n this.startReject = null\n }\n\n // Once stop() is waiting, settle it on the terminal `end` event so we\n // capture the last finalized transcript from the browser.\n if (this.stopResolve || this.stopReject) {\n if (this.lastError) {\n this.stopReject?.(this.lastError)\n } else {\n this.stopResolve?.(normalizeSpeechText(this.finalTranscript))\n }\n\n this.stopResolve = null\n this.stopReject = null\n }\n }\n\n const started = new Promise<void>((resolve, reject) => {\n this.startResolve = resolve\n this.startReject = reject\n })\n\n try {\n recognition.start()\n } catch (error) {\n this.clearRecognition()\n throw toError(error, \"Browser transcription failed to start\")\n }\n\n try {\n await started\n } catch (error) {\n this.clearRecognition()\n throw toError(error, \"Browser transcription failed to start\")\n }\n }\n\n /**\n * Stop the current recognition session and resolve with the final transcript.\n */\n async stop(): Promise<string> {\n if (!this.recognition) {\n if (this.lastError) {\n throw this.lastError\n }\n\n return normalizeSpeechText(this.finalTranscript)\n }\n\n if (this.hasEnded) {\n const transcript = normalizeSpeechText(this.finalTranscript)\n const error = this.lastError\n this.clearRecognition()\n\n if (error) {\n throw error\n }\n\n return transcript\n }\n\n const recognition = this.recognition\n\n const transcript = await new Promise<string>((resolve, reject) => {\n this.stopResolve = resolve\n this.stopReject = reject\n\n try {\n recognition.stop()\n } catch (error) {\n reject(toError(error, \"Browser transcription failed to stop\"))\n }\n }).finally(() => {\n this.clearRecognition()\n })\n\n return normalizeSpeechText(transcript)\n }\n\n /**\n * Abort the current recognition session and reset the service for reuse.\n */\n dispose(): void {\n if (this.recognition) {\n try {\n this.recognition.abort()\n } catch {\n // Ignore abort failures during cleanup.\n }\n }\n\n this.startReject?.(new Error(\"Browser transcription aborted\"))\n this.stopResolve?.(normalizeSpeechText(this.finalTranscript))\n this.startResolve = null\n this.startReject = null\n this.stopResolve = null\n this.stopReject = null\n this.clearRecognition()\n this.resetSessionState()\n }\n\n private clearRecognition(): void {\n if (!this.recognition) return\n\n // Drop event handlers explicitly so a late browser callback cannot mutate\n // the service after the turn has already moved on.\n this.recognition.onstart = null\n this.recognition.onresult = null\n this.recognition.onerror = null\n this.recognition.onend = null\n this.recognition = null\n }\n\n private resetSessionState(): void {\n this.finalTranscript = \"\"\n this.hasStarted = false\n this.hasEnded = false\n this.lastError = null\n // Clear the live transcript view at the start and end of each turn.\n this.partialCallback?.(\"\")\n }\n}\n","import type { Point } from \"./types\"\n\n/**\n * Bezier flight animation for cursor pointing.\n */\n\n/**\n * Quadratic bezier curve: B(t) = (1-t)²P₀ + 2(1-t)t·P₁ + t²P₂\n */\nfunction quadraticBezier(p0: Point, p1: Point, p2: Point, t: number): Point {\n const oneMinusT = 1 - t\n return {\n x: oneMinusT * oneMinusT * p0.x + 2 * oneMinusT * t * p1.x + t * t * p2.x,\n y: oneMinusT * oneMinusT * p0.y + 2 * oneMinusT * t * p1.y + t * t * p2.y,\n }\n}\n\n/**\n * Bezier tangent (derivative): B'(t) = 2(1-t)(P₁-P₀) + 2t(P₂-P₁)\n */\nfunction bezierTangent(p0: Point, p1: Point, p2: Point, t: number): Point {\n const oneMinusT = 1 - t\n return {\n x: 2 * oneMinusT * (p1.x - p0.x) + 2 * t * (p2.x - p1.x),\n y: 2 * oneMinusT * (p1.y - p0.y) + 2 * t * (p2.y - p1.y),\n }\n}\n\n/**\n * Ease-in-out cubic for smooth acceleration/deceleration\n */\nfunction easeInOutCubic(t: number): number {\n return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2\n}\n\nexport interface BezierFlightCallbacks {\n onFrame: (position: Point, rotation: number, scale: number) => void\n onComplete: () => void\n}\n\n/**\n * Animate cursor along a parabolic bezier arc from start to end.\n * Used when the AI points at a UI element.\n *\n * @param from - Starting position\n * @param to - Target position\n * @param durationMs - Flight duration in milliseconds\n * @param callbacks - Frame and completion callbacks\n * @returns Cancel function to stop the animation\n */\nexport function animateBezierFlight(\n from: Point,\n to: Point,\n durationMs: number,\n callbacks: BezierFlightCallbacks,\n): () => void {\n const startTime = performance.now()\n const distance = Math.hypot(to.x - from.x, to.y - from.y)\n\n // Control point: offset upward by 20% of distance (creates parabolic arc)\n const controlPoint: Point = {\n x: (from.x + to.x) / 2,\n y: Math.min(from.y, to.y) - distance * 0.2,\n }\n\n let animationFrameId: number\n\n function animate(now: number) {\n const elapsed = now - startTime\n const linearProgress = Math.min(elapsed / durationMs, 1)\n const easedProgress = easeInOutCubic(linearProgress)\n\n const position = quadraticBezier(from, controlPoint, to, easedProgress)\n const tangent = bezierTangent(from, controlPoint, to, easedProgress)\n const rotation = Math.atan2(tangent.y, tangent.x)\n\n // Scale pulse: grows to 1.3x at midpoint, returns to 1x\n const scale = 1 + Math.sin(linearProgress * Math.PI) * 0.3\n\n callbacks.onFrame(position, rotation, scale)\n\n if (linearProgress < 1) {\n animationFrameId = requestAnimationFrame(animate)\n } else {\n callbacks.onComplete()\n }\n }\n\n animationFrameId = requestAnimationFrame(animate)\n\n return () => cancelAnimationFrame(animationFrameId)\n}\n","import {\n $buddyPosition,\n $buddyRotation,\n $buddyScale,\n $cursorPosition,\n $pointingTarget,\n} from \"../atoms\"\nimport { animateBezierFlight } from \"../bezier\"\nimport type { PointerControllerPort, PointingTarget } from \"../types\"\n\nconst POINTING_LOCK_TIMEOUT_MS = 10_000\n\ntype PointerMode = \"follow\" | \"flying\" | \"anchored\"\n\n/**\n * Controller for cursor pointing behavior.\n * Manages the pointer state machine (follow -> flying -> anchored -> follow)\n * and cursor animation.\n */\nexport class PointerController implements PointerControllerPort {\n private mode: PointerMode = \"follow\"\n private cancelAnimation: (() => void) | null = null\n private releaseTimeout: ReturnType<typeof setTimeout> | null = null\n private listeners = new Set<() => void>()\n\n /**\n * Animate cursor to point at a target.\n */\n pointAt(target: PointingTarget): void {\n // Clear any previous pointing state\n this.release()\n\n this.mode = \"flying\"\n $pointingTarget.set(target)\n\n const startPos = $buddyPosition.get()\n const endPos = { x: target.x, y: target.y }\n\n this.cancelAnimation = animateBezierFlight(startPos, endPos, 800, {\n onFrame: (position, rotation, scale) => {\n $buddyPosition.set(position)\n $buddyRotation.set(rotation)\n $buddyScale.set(scale)\n },\n onComplete: () => {\n this.cancelAnimation = null\n this.mode = \"anchored\"\n $buddyPosition.set(endPos)\n $buddyRotation.set(0)\n $buddyScale.set(1)\n this.scheduleRelease()\n this.notify()\n },\n })\n\n this.notify()\n }\n\n /**\n * Release the cursor from pointing mode back to follow mode.\n */\n release(): void {\n // Cancel any in-progress animation\n if (this.cancelAnimation) {\n this.cancelAnimation()\n this.cancelAnimation = null\n }\n\n // Clear release timeout\n if (this.releaseTimeout) {\n clearTimeout(this.releaseTimeout)\n this.releaseTimeout = null\n }\n\n // Reset to follow mode\n this.mode = \"follow\"\n $pointingTarget.set(null)\n $buddyPosition.set($cursorPosition.get())\n $buddyRotation.set(0)\n $buddyScale.set(1)\n\n this.notify()\n }\n\n /**\n * Check if cursor is currently pointing (flying or anchored).\n */\n isPointing(): boolean {\n return this.mode !== \"follow\"\n }\n\n /**\n * Get current pointer mode.\n */\n getMode(): PointerMode {\n return this.mode\n }\n\n /**\n * Subscribe to pointer state changes.\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /**\n * Update buddy position to follow cursor when in follow mode.\n * Call this on cursor position changes.\n */\n updateFollowPosition(): void {\n if (this.mode === \"follow\") {\n $buddyPosition.set($cursorPosition.get())\n $buddyRotation.set(0)\n $buddyScale.set(1)\n }\n }\n\n private scheduleRelease(): void {\n this.releaseTimeout = setTimeout(() => {\n this.releaseTimeout = null\n this.release()\n }, POINTING_LOCK_TIMEOUT_MS)\n }\n\n private notify(): void {\n this.listeners.forEach((listener) => listener())\n }\n}\n","/**\n * Annotation drawing for screenshots.\n * Draws numbered markers on interactive elements.\n */\n\nimport type { MarkerMap } from \"./elements\"\n\n/**\n * Annotation style configuration.\n */\ninterface AnnotationStyle {\n /** Border color for marker boxes */\n borderColor: string\n /** Background color for number labels */\n labelBackground: string\n /** Text color for number labels */\n labelColor: string\n /** Border width in pixels */\n borderWidth: number\n /** Font size for labels */\n fontSize: number\n /** Padding around label text */\n labelPadding: number\n}\n\nconst DEFAULT_STYLE: AnnotationStyle = {\n borderColor: \"rgba(255, 0, 0, 0.8)\",\n labelBackground: \"rgba(255, 0, 0, 0.9)\",\n labelColor: \"#ffffff\",\n borderWidth: 2,\n fontSize: 15,\n labelPadding: 4,\n}\n\n/**\n * Draw annotation markers onto a canvas.\n * Modifies the canvas in place.\n *\n * @param ctx Canvas 2D context to draw on\n * @param markers Marker map from element discovery\n * @param style Optional style overrides\n */\nexport function drawAnnotations(\n ctx: CanvasRenderingContext2D,\n markers: MarkerMap,\n style: Partial<AnnotationStyle> = {},\n): void {\n const s = { ...DEFAULT_STYLE, ...style }\n\n ctx.save()\n\n for (const marker of markers.values()) {\n const { rect, id } = marker\n\n // Draw border box\n ctx.strokeStyle = s.borderColor\n ctx.lineWidth = s.borderWidth\n ctx.strokeRect(rect.left, rect.top, rect.width, rect.height)\n\n // Draw label\n const label = String(id)\n ctx.font = `bold ${s.fontSize}px monospace`\n const textMetrics = ctx.measureText(label)\n const textWidth = textMetrics.width\n const textHeight = s.fontSize\n\n // Label position: top-left of element, or inside if near viewport top\n const labelWidth = textWidth + s.labelPadding * 2\n const labelHeight = textHeight + s.labelPadding\n const labelX = rect.left - s.borderWidth\n const labelY =\n rect.top < labelHeight + 4 ? rect.top + 2 : rect.top - labelHeight\n\n // Label background\n ctx.fillStyle = s.labelBackground\n ctx.beginPath()\n ctx.roundRect(labelX, labelY, labelWidth, labelHeight, 2)\n ctx.fill()\n\n // Label text\n ctx.fillStyle = s.labelColor\n ctx.textBaseline = \"top\"\n ctx.fillText(label, labelX + s.labelPadding, labelY + s.labelPadding / 2)\n }\n\n ctx.restore()\n}\n\n/**\n * Create an annotated copy of a canvas.\n * Does not modify the original canvas.\n *\n * @param sourceCanvas Original screenshot canvas\n * @param markers Marker map from element discovery\n * @returns New canvas with annotations drawn\n */\nexport function createAnnotatedCanvas(\n sourceCanvas: HTMLCanvasElement,\n markers: MarkerMap,\n): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\")\n canvas.width = sourceCanvas.width\n canvas.height = sourceCanvas.height\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2D context\")\n }\n\n // Copy source image\n ctx.drawImage(sourceCanvas, 0, 0)\n\n // Draw annotations\n drawAnnotations(ctx, markers)\n\n return canvas\n}\n\n/**\n * Generate marker context string for AI prompt.\n * Lists available markers with their descriptions.\n *\n * @param markers Marker map from element discovery\n * @returns Formatted string listing markers\n */\nexport function generateMarkerContext(markers: MarkerMap): string {\n if (markers.size === 0) {\n return \"No interactive elements detected.\"\n }\n\n const lines = [\"Interactive elements (use marker number to point):\"]\n\n for (const marker of markers.values()) {\n lines.push(` ${marker.id}: ${marker.description}`)\n }\n\n return lines.join(\"\\n\")\n}\n","/**\n * Element discovery for annotated screenshots.\n * Finds visible interactive elements and assigns marker IDs.\n */\n\n/** Max characters for element descriptions passed to the model. */\nconst MAX_DESCRIPTION_LENGTH = 50\n\n/** Pixels tolerance for grouping elements into the same visual row. */\nconst ROW_TOLERANCE_PX = 20\n\n/**\n * Interactive element selectors - elements users would want to click/interact with.\n * Mirrors accessibility roles from agent-browser but using CSS selectors.\n */\nconst INTERACTIVE_SELECTORS = [\n // Buttons\n \"button\",\n '[role=\"button\"]',\n 'input[type=\"button\"]',\n 'input[type=\"submit\"]',\n 'input[type=\"reset\"]',\n\n // Links\n \"a[href]\",\n '[role=\"link\"]',\n\n // Form inputs\n 'input:not([type=\"hidden\"])',\n \"textarea\",\n \"select\",\n '[role=\"textbox\"]',\n '[role=\"searchbox\"]',\n '[role=\"combobox\"]',\n '[role=\"listbox\"]',\n '[role=\"slider\"]',\n '[role=\"spinbutton\"]',\n\n // Checkboxes and radios\n '[role=\"checkbox\"]',\n '[role=\"radio\"]',\n '[role=\"switch\"]',\n\n // Menu items\n '[role=\"menuitem\"]',\n '[role=\"menuitemcheckbox\"]',\n '[role=\"menuitemradio\"]',\n '[role=\"option\"]',\n\n // Tabs\n '[role=\"tab\"]',\n '[role=\"treeitem\"]',\n\n // Media controls\n \"video\",\n \"audio\",\n\n // Custom interactive elements (opt-in)\n \"[data-cursor-buddy-interactive]\",\n]\n\n/**\n * Element marker with reference to actual DOM element.\n */\nexport interface ElementMarker {\n /** Sequential marker ID (1, 2, 3...) */\n id: number\n /** Reference to the actual DOM element */\n element: Element\n /** Bounding rect at time of capture */\n rect: DOMRect\n /** Brief description for AI context */\n description: string\n}\n\n/**\n * Map of marker ID to element marker.\n */\nexport type MarkerMap = Map<number, ElementMarker>\n\n/**\n * Check if an element is visible in the viewport.\n */\nfunction isElementVisible(\n element: Element,\n rect: DOMRect = element.getBoundingClientRect(),\n): boolean {\n // Has size\n if (rect.width <= 0 || rect.height <= 0) return false\n\n // In viewport\n if (\n rect.bottom < 0 ||\n rect.top > window.innerHeight ||\n rect.right < 0 ||\n rect.left > window.innerWidth\n ) {\n return false\n }\n\n // Not hidden via CSS\n const style = window.getComputedStyle(element)\n if (style.visibility === \"hidden\" || style.display === \"none\") return false\n if (Number.parseFloat(style.opacity) === 0) return false\n\n return true\n}\n\nfunction truncateDescription(value: string): string {\n return value.slice(0, MAX_DESCRIPTION_LENGTH)\n}\n\n/**\n * Generate a brief description for an element.\n */\nfunction describeElement(element: Element): string {\n const tag = element.tagName.toLowerCase()\n\n // Try aria-label first\n const ariaLabel = element.getAttribute(\"aria-label\")\n if (ariaLabel) return truncateDescription(ariaLabel)\n\n // Try text content for buttons/links\n if (tag === \"button\" || tag === \"a\") {\n const text = element.textContent?.trim()\n if (text) return truncateDescription(text)\n }\n\n // Try placeholder for inputs\n if (tag === \"input\" || tag === \"textarea\") {\n const placeholder = element.getAttribute(\"placeholder\")\n if (placeholder) return truncateDescription(placeholder)\n\n const type = element.getAttribute(\"type\") || \"text\"\n return `${type} input`\n }\n\n // Try alt for images\n if (tag === \"img\") {\n const alt = element.getAttribute(\"alt\")\n if (alt) return truncateDescription(alt)\n return \"image\"\n }\n\n // Try role\n const role = element.getAttribute(\"role\")\n if (role) return role\n\n // Fallback to tag name\n return tag\n}\n\ninterface VisibleInteractiveElement {\n element: Element\n rect: DOMRect\n}\n\nfunction collectVisibleInteractiveElements(): VisibleInteractiveElement[] {\n const selector = INTERACTIVE_SELECTORS.join(\",\")\n const allElements = document.querySelectorAll(selector)\n const visible: VisibleInteractiveElement[] = []\n\n for (const element of allElements) {\n const rect = element.getBoundingClientRect()\n if (!isElementVisible(element, rect)) continue\n\n visible.push({ element, rect })\n }\n\n visible.sort((a, b) => {\n // Primary: top position, grouped into approximate rows\n const rowDiff =\n Math.floor(a.rect.top / ROW_TOLERANCE_PX) -\n Math.floor(b.rect.top / ROW_TOLERANCE_PX)\n if (rowDiff !== 0) return rowDiff\n\n // Secondary: left position\n return a.rect.left - b.rect.left\n })\n\n return visible\n}\n\n/**\n * Find all visible interactive elements in the viewport.\n * Returns elements sorted by visual position (top-left to bottom-right).\n */\nexport function findInteractiveElements(): Element[] {\n return collectVisibleInteractiveElements().map(({ element }) => element)\n}\n\n/**\n * Create marker map from visible interactive elements.\n * Assigns sequential IDs starting from 1.\n */\nexport function createMarkerMap(): MarkerMap {\n const elements = collectVisibleInteractiveElements()\n const map: MarkerMap = new Map()\n\n elements.forEach(({ element, rect }, index) => {\n const id = index + 1\n map.set(id, {\n id,\n element,\n rect,\n description: describeElement(element),\n })\n })\n\n return map\n}\n\n/**\n * Get the center point of an element in viewport coordinates.\n */\nexport function getElementCenter(element: Element): { x: number; y: number } {\n const rect = element.getBoundingClientRect()\n return {\n x: Math.round(rect.left + rect.width / 2),\n y: Math.round(rect.top + rect.height / 2),\n }\n}\n\n/**\n * Resolve a marker ID to viewport coordinates.\n * Returns null if marker not found or element no longer visible.\n */\nexport function resolveMarkerToCoordinates(\n markerMap: MarkerMap,\n markerId: number,\n): { x: number; y: number } | null {\n const marker = markerMap.get(markerId)\n if (!marker) return null\n\n // Check element still exists and is visible\n if (!document.contains(marker.element)) return null\n if (!isElementVisible(marker.element)) return null\n\n return getElementCenter(marker.element)\n}\n","import html2canvas from \"html2canvas-pro\"\nimport type { AnnotatedScreenshotResult, ScreenshotResult } from \"../types\"\nimport { createAnnotatedCanvas, generateMarkerContext } from \"./annotations\"\nimport { createMarkerMap } from \"./elements\"\n\nconst CLONE_RESOURCE_TIMEOUT_MS = 3000\n\n/** Maximum width for compressed screenshots (maintains aspect ratio) */\nconst MAX_SCREENSHOT_WIDTH = 1280\n\n/** JPEG quality for compressed screenshots (0-1) */\nconst JPEG_QUALITY = 0.8\n\n/**\n * Compression result with compressed image data and dimensions.\n */\ninterface CompressionResult {\n /** Base64-encoded compressed image data */\n imageData: string\n /** Width of the compressed image */\n width: number\n /** Height of the compressed image */\n height: number\n}\n\n/**\n * Compress a canvas image by downscaling and converting to JPEG.\n * Maintains aspect ratio and falls back to original if compression fails.\n *\n * @param sourceCanvas - The source canvas to compress\n * @param maxWidth - Maximum width for the compressed image (default: MAX_SCREENSHOT_WIDTH)\n * @param quality - JPEG quality 0-1 (default: JPEG_QUALITY)\n * @returns Compression result with compressed image data and dimensions\n */\nfunction compressImage(\n sourceCanvas: HTMLCanvasElement,\n maxWidth: number = MAX_SCREENSHOT_WIDTH,\n quality: number = JPEG_QUALITY,\n): CompressionResult {\n const sourceWidth = sourceCanvas.width\n const sourceHeight = sourceCanvas.height\n\n // If source is already smaller than max width, just convert to JPEG\n if (sourceWidth <= maxWidth) {\n return {\n imageData: sourceCanvas.toDataURL(\"image/jpeg\", quality),\n width: sourceWidth,\n height: sourceHeight,\n }\n }\n\n // Calculate scaled dimensions maintaining aspect ratio\n const scale = maxWidth / sourceWidth\n const targetWidth = Math.round(maxWidth)\n const targetHeight = Math.round(sourceHeight * scale)\n\n // Create canvas for compressed image\n const canvas = document.createElement(\"canvas\")\n canvas.width = targetWidth\n canvas.height = targetHeight\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) {\n // Fallback: return original as JPEG\n return {\n imageData: sourceCanvas.toDataURL(\"image/jpeg\", quality),\n width: sourceWidth,\n height: sourceHeight,\n }\n }\n\n // Use better quality scaling\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = \"high\"\n\n // Draw scaled image\n ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight)\n\n // Export as JPEG\n return {\n imageData: canvas.toDataURL(\"image/jpeg\", quality),\n width: targetWidth,\n height: targetHeight,\n }\n}\n\nfunction getCaptureMetrics() {\n return {\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n }\n}\n\nfunction waitForNextPaint(doc: Document): Promise<void> {\n const view = doc.defaultView\n if (!view?.requestAnimationFrame) return Promise.resolve()\n\n return new Promise((resolve) => {\n view.requestAnimationFrame(() => {\n view.requestAnimationFrame(() => resolve())\n })\n })\n}\n\nfunction isStylesheetReady(link: HTMLLinkElement): boolean {\n const sheet = link.sheet\n if (!sheet) return false\n\n try {\n void sheet.cssRules\n return true\n } catch (error) {\n return error instanceof DOMException && error.name === \"SecurityError\"\n }\n}\n\nfunction waitForStylesheetLink(link: HTMLLinkElement): Promise<void> {\n if (isStylesheetReady(link)) return Promise.resolve()\n\n return new Promise((resolve) => {\n let settled = false\n let timeoutId = 0\n\n const finish = () => {\n if (settled) return\n settled = true\n window.clearTimeout(timeoutId)\n link.removeEventListener(\"load\", handleReady)\n link.removeEventListener(\"error\", handleReady)\n resolve()\n }\n\n const handleReady = () => {\n if (isStylesheetReady(link)) {\n finish()\n return\n }\n\n window.requestAnimationFrame(() => {\n if (isStylesheetReady(link)) {\n finish()\n }\n })\n }\n\n timeoutId = window.setTimeout(finish, CLONE_RESOURCE_TIMEOUT_MS)\n link.addEventListener(\"load\", handleReady, { once: true })\n link.addEventListener(\"error\", finish, { once: true })\n\n handleReady()\n })\n}\n\nasync function waitForClonedDocumentStyles(doc: Document): Promise<void> {\n const stylesheetLinks = Array.from(\n doc.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"][href]'),\n )\n\n await Promise.all(stylesheetLinks.map(waitForStylesheetLink))\n\n if (doc.fonts?.ready) {\n await doc.fonts.ready\n }\n\n await waitForNextPaint(doc)\n}\n\nfunction getHtml2CanvasOptions(\n captureMetrics: ReturnType<typeof getCaptureMetrics>,\n) {\n return {\n scale: window.devicePixelRatio,\n useCORS: true,\n logging: false,\n width: captureMetrics.viewportWidth,\n height: captureMetrics.viewportHeight,\n windowWidth: captureMetrics.viewportWidth,\n windowHeight: captureMetrics.viewportHeight,\n x: window.scrollX,\n y: window.scrollY,\n scrollX: window.scrollX,\n scrollY: window.scrollY,\n // In production Next.js emits external stylesheet links; wait for the\n // cloned iframe to finish applying them before html2canvas renders.\n onclone: async (doc: Document) => {\n await waitForClonedDocumentStyles(doc)\n },\n } as const\n}\n\n/**\n * Create a fallback canvas when screenshot capture fails.\n * Returns a simple gray canvas with an error message.\n */\nfunction createFallbackCanvas(): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\")\n canvas.width = window.innerWidth\n canvas.height = window.innerHeight\n\n const ctx = canvas.getContext(\"2d\")\n if (ctx) {\n ctx.fillStyle = \"#f0f0f0\"\n ctx.fillRect(0, 0, canvas.width, canvas.height)\n ctx.fillStyle = \"#666\"\n ctx.font = \"16px sans-serif\"\n ctx.textAlign = \"center\"\n ctx.fillText(\"Screenshot unavailable\", canvas.width / 2, canvas.height / 2)\n }\n\n return canvas\n}\n\n/**\n * Capture a screenshot of the current viewport.\n * Uses html2canvas to render the DOM to a canvas, then compresses to JPEG.\n * Falls back to a placeholder if capture fails (e.g., due to unsupported CSS).\n */\nexport async function captureViewport(): Promise<ScreenshotResult> {\n const captureMetrics = getCaptureMetrics()\n let canvas: HTMLCanvasElement\n\n try {\n canvas = await html2canvas(\n document.body,\n getHtml2CanvasOptions(captureMetrics),\n )\n } catch {\n canvas = createFallbackCanvas()\n }\n\n // Compress the screenshot (with fallback to uncompressed on error)\n let compressed: CompressionResult\n try {\n compressed = compressImage(canvas)\n } catch {\n // Fallback: use uncompressed PNG\n compressed = {\n imageData: canvas.toDataURL(\"image/png\"),\n width: canvas.width,\n height: canvas.height,\n }\n }\n\n return {\n imageData: compressed.imageData,\n width: compressed.width,\n height: compressed.height,\n viewportWidth: captureMetrics.viewportWidth,\n viewportHeight: captureMetrics.viewportHeight,\n }\n}\n\n/**\n * Capture an annotated screenshot of the current viewport.\n * Interactive elements are marked with numbered labels.\n * Returns both the annotated image and a marker map for resolving IDs.\n */\nexport async function captureAnnotatedViewport(): Promise<AnnotatedScreenshotResult> {\n const captureMetrics = getCaptureMetrics()\n\n // 1. Discover interactive elements BEFORE capturing screenshot\n // (so rects are accurate to what's visible)\n const markerMap = createMarkerMap()\n\n // 2. Capture screenshot\n let sourceCanvas: HTMLCanvasElement\n try {\n sourceCanvas = await html2canvas(\n document.body,\n getHtml2CanvasOptions(captureMetrics),\n )\n } catch {\n sourceCanvas = createFallbackCanvas()\n }\n\n // 3. Create a fresh canvas and draw annotations on it\n // (html2canvas leaves dirty context state - transforms, clipping, etc.)\n const canvas =\n markerMap.size > 0\n ? createAnnotatedCanvas(sourceCanvas, markerMap)\n : sourceCanvas\n\n // 4. Generate marker context for AI\n const markerContext = generateMarkerContext(markerMap)\n\n // 5. Compress the screenshot (with fallback to uncompressed on error)\n let compressed: CompressionResult\n try {\n compressed = compressImage(canvas)\n } catch {\n // Fallback: use uncompressed PNG\n compressed = {\n imageData: canvas.toDataURL(\"image/png\"),\n width: canvas.width,\n height: canvas.height,\n }\n }\n\n return {\n imageData: compressed.imageData,\n width: compressed.width,\n height: compressed.height,\n viewportWidth: captureMetrics.viewportWidth,\n viewportHeight: captureMetrics.viewportHeight,\n markerMap,\n markerContext,\n }\n}\n","import type {\n AnnotatedScreenshotResult,\n ScreenCapturePort,\n ScreenshotResult,\n} from \"../types\"\nimport { captureAnnotatedViewport, captureViewport } from \"../utils/screenshot\"\n\n/**\n * Framework-agnostic service for capturing viewport screenshots.\n */\nexport class ScreenCaptureService implements ScreenCapturePort {\n /**\n * Capture a screenshot of the current viewport.\n * @returns Screenshot result with image data and dimensions\n */\n async capture(): Promise<ScreenshotResult> {\n return captureViewport()\n }\n\n /**\n * Capture an annotated screenshot with marker overlays.\n * Interactive elements are marked with numbered labels.\n * @returns Annotated screenshot result with marker map\n */\n async captureAnnotated(): Promise<AnnotatedScreenshotResult> {\n return captureAnnotatedViewport()\n }\n}\n","import { toError } from \"../utils/error\"\n\nexport type SpeechPlaybackTask = () => Promise<void>\n\ninterface TTSPlaybackQueueOptions {\n onError?: (error: Error) => void\n onPlaybackStart?: () => void\n signal?: AbortSignal\n prepare: (text: string, signal?: AbortSignal) => Promise<SpeechPlaybackTask>\n}\n\n/**\n * Queues sentence-level speech preparation immediately while keeping playback\n * strictly ordered.\n *\n * Preparation is allowed to run ahead of playback so server synthesis can\n * overlap with the currently playing segment, but the returned playback tasks\n * still execute one-by-one in enqueue order.\n */\nexport class TTSPlaybackQueue {\n private error: Error | null = null\n private hasStartedPlayback = false\n private onError?: (error: Error) => void\n private onPlaybackStart?: () => void\n private playbackChain = Promise.resolve()\n private prepare: (\n text: string,\n signal?: AbortSignal,\n ) => Promise<SpeechPlaybackTask>\n private signal?: AbortSignal\n\n constructor(options: TTSPlaybackQueueOptions) {\n this.onError = options.onError\n this.onPlaybackStart = options.onPlaybackStart\n this.prepare = options.prepare\n this.signal = options.signal\n }\n\n /**\n * Queue a speakable text segment.\n */\n enqueue(text: string): void {\n const normalizedText = text.trim()\n if (!normalizedText || this.error || this.signal?.aborted) return\n\n // Kick off preparation immediately so synthesis/download work can overlap\n // with the segment currently playing.\n const preparedPlaybackTask = this.prepare(normalizedText, this.signal)\n\n // Preparation can finish after the queue has already been aborted. Attach\n // a background rejection handler so those late failures are still recorded\n // and do not surface as unhandled promise rejections in tests or apps.\n void preparedPlaybackTask.catch((error) => {\n this.fail(toError(error))\n })\n\n const nextStep = this.playbackChain.then(async () => {\n if (this.signal?.aborted) return\n\n const play = await preparedPlaybackTask\n if (this.signal?.aborted) return\n\n if (!this.hasStartedPlayback) {\n this.hasStartedPlayback = true\n this.onPlaybackStart?.()\n }\n\n await play()\n })\n\n this.playbackChain = nextStep.catch((error) => {\n this.fail(toError(error))\n })\n }\n\n /**\n * Wait until every queued segment has either played or the queue failed.\n */\n async waitForCompletion(): Promise<void> {\n await this.playbackChain\n\n if (this.error) {\n throw this.error\n }\n }\n\n private fail(error: Error): void {\n if (this.error) return\n\n this.error = error\n this.onError?.(error)\n }\n}\n","/**\n * Audio conversion utilities for voice capture.\n * Converts Float32 audio data to WAV format for server transcription.\n */\n\n/**\n * Merge multiple Float32Array chunks into a single array\n */\nexport function mergeAudioChunks(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0)\n const result = new Float32Array(totalLength)\n\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.length\n }\n\n return result\n}\n\n/**\n * Convert Float32 audio data to 16-bit PCM\n */\nfunction floatTo16BitPCM(\n output: DataView,\n offset: number,\n input: Float32Array,\n): void {\n for (let i = 0; i < input.length; i++, offset += 2) {\n const sample = Math.max(-1, Math.min(1, input[i]))\n output.setInt16(\n offset,\n sample < 0 ? sample * 0x8000 : sample * 0x7fff,\n true,\n )\n }\n}\n\n/**\n * Write a string to a DataView\n */\nfunction writeString(view: DataView, offset: number, string: string): void {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i))\n }\n}\n\n/**\n * Encode Float32 audio data as a WAV file\n */\nexport function encodeWAV(samples: Float32Array, sampleRate: number): Blob {\n const numChannels = 1\n const bitsPerSample = 16\n const bytesPerSample = bitsPerSample / 8\n const blockAlign = numChannels * bytesPerSample\n\n const dataLength = samples.length * bytesPerSample\n const buffer = new ArrayBuffer(44 + dataLength)\n const view = new DataView(buffer)\n\n // RIFF header\n writeString(view, 0, \"RIFF\")\n view.setUint32(4, 36 + dataLength, true)\n writeString(view, 8, \"WAVE\")\n\n // fmt chunk\n writeString(view, 12, \"fmt \")\n view.setUint32(16, 16, true) // chunk size\n view.setUint16(20, 1, true) // audio format (PCM)\n view.setUint16(22, numChannels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, sampleRate * blockAlign, true) // byte rate\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n\n // data chunk\n writeString(view, 36, \"data\")\n view.setUint32(40, dataLength, true)\n\n floatTo16BitPCM(view, 44, samples)\n\n return new Blob([buffer], { type: \"audio/wav\" })\n}\n","/**\n * AudioWorklet processor code for voice capture.\n * Inlined as a blob URL to avoid separate file serving requirements.\n */\nconst workletCode = `\nclass AudioCaptureProcessor extends AudioWorkletProcessor {\n constructor() {\n super()\n this.isRecording = true\n this.audioChunkSize = 2048\n this.audioBuffer = new Float32Array(this.audioChunkSize)\n this.audioBufferIndex = 0\n this.levelFramesPerUpdate = 4\n this.levelFrameCount = 0\n this.levelRmsSum = 0\n this.levelPeak = 0\n\n this.port.onmessage = (event) => {\n if (event.data?.type === \"flush\") {\n this.flushAudio()\n this.flushLevel()\n this.port.postMessage({ type: \"flush-complete\" })\n }\n }\n }\n\n flushAudio() {\n if (this.audioBufferIndex === 0) return\n\n const chunk = this.audioBuffer.slice(0, this.audioBufferIndex)\n this.port.postMessage({\n type: \"audio\",\n data: chunk\n })\n this.audioBufferIndex = 0\n }\n\n flushLevel() {\n if (this.levelFrameCount === 0) return\n\n this.port.postMessage({\n type: \"level\",\n rms: this.levelRmsSum / this.levelFrameCount,\n peak: this.levelPeak\n })\n\n this.levelFrameCount = 0\n this.levelRmsSum = 0\n this.levelPeak = 0\n }\n\n process(inputs) {\n if (!this.isRecording) return false\n\n const input = inputs[0]\n if (input && input.length > 0) {\n const channelData = input[0]\n let sum = 0\n let peak = 0\n for (let i = 0; i < channelData.length; i++) {\n const sample = channelData[i]\n sum += sample * sample\n const absolute = Math.abs(sample)\n if (absolute > peak) peak = absolute\n }\n\n this.levelRmsSum += Math.sqrt(sum / channelData.length)\n this.levelPeak = Math.max(this.levelPeak, peak)\n this.levelFrameCount += 1\n\n if (this.levelFrameCount >= this.levelFramesPerUpdate) {\n this.flushLevel()\n }\n\n let readOffset = 0\n while (readOffset < channelData.length) {\n const remaining = this.audioBuffer.length - this.audioBufferIndex\n const copyLength = Math.min(remaining, channelData.length - readOffset)\n\n this.audioBuffer.set(\n channelData.subarray(readOffset, readOffset + copyLength),\n this.audioBufferIndex\n )\n\n this.audioBufferIndex += copyLength\n readOffset += copyLength\n\n if (this.audioBufferIndex >= this.audioBuffer.length) {\n this.flushAudio()\n }\n }\n }\n\n return true\n }\n}\n\nregisterProcessor(\"audio-capture-processor\", AudioCaptureProcessor)\n`\n\nlet cachedBlobURL: string | null = null\n\n/**\n * Create a blob URL for the audio worklet processor.\n * Caches the URL to avoid creating multiple blobs.\n */\nexport function createWorkletBlobURL(): string {\n if (!cachedBlobURL) {\n const blob = new Blob([workletCode], { type: \"application/javascript\" })\n cachedBlobURL = URL.createObjectURL(blob)\n }\n return cachedBlobURL\n}\n\n/**\n * Clean up the cached worklet blob URL.\n * Call this when the app unmounts if needed.\n */\nexport function revokeWorkletBlobURL(): void {\n if (cachedBlobURL) {\n URL.revokeObjectURL(cachedBlobURL)\n cachedBlobURL = null\n }\n}\n","import type { VoiceCapturePort } from \"../types\"\nimport { encodeWAV, mergeAudioChunks } from \"../utils/audio\"\nimport { createWorkletBlobURL } from \"../utils/audio-worklet\"\n\nconst SAMPLE_RATE = 16000\nconst AUDIO_LEVEL_NOISE_GATE = 0.0005\nconst AUDIO_LEVEL_INPUT_GAIN = 600\nconst AUDIO_LEVEL_ATTACK = 0.7\nconst AUDIO_LEVEL_RELEASE = 0.25\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\nfunction normalizeAudioLevel(rms: number): number {\n const gatedRms = Math.max(0, rms - AUDIO_LEVEL_NOISE_GATE)\n return clamp(\n Math.log1p(gatedRms * AUDIO_LEVEL_INPUT_GAIN) /\n Math.log1p(AUDIO_LEVEL_INPUT_GAIN),\n 0,\n 1,\n )\n}\n\nfunction smoothAudioLevel(current: number, target: number): number {\n const smoothing = target > current ? AUDIO_LEVEL_ATTACK : AUDIO_LEVEL_RELEASE\n return current + (target - current) * smoothing\n}\n\n/**\n * Framework-agnostic service for voice capture using AudioWorkletNode.\n */\nexport class VoiceCaptureService implements VoiceCapturePort {\n private audioContext: AudioContext | null = null\n private workletNode: AudioWorkletNode | null = null\n private sourceNode: MediaStreamAudioSourceNode | null = null\n private silentGainNode: GainNode | null = null\n private stream: MediaStream | null = null\n private chunks: Float32Array[] = []\n private levelCallback: ((level: number) => void) | null = null\n private visualLevel = 0\n private flushResolve: (() => void) | null = null\n\n /**\n * Register a callback to receive audio level updates (0-1).\n * Called at ~60fps during recording for waveform visualization.\n */\n onLevel(callback: (level: number) => void): void {\n this.levelCallback = callback\n }\n\n /**\n * Start recording audio from the microphone.\n * @throws Error if microphone access is denied\n */\n async start(): Promise<void> {\n this.chunks = []\n this.visualLevel = 0\n\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n sampleRate: SAMPLE_RATE,\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n },\n })\n this.stream = stream\n\n const audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })\n this.audioContext = audioContext\n await audioContext.resume()\n\n // Load worklet from blob URL\n const workletURL = createWorkletBlobURL()\n await audioContext.audioWorklet.addModule(workletURL)\n\n const source = audioContext.createMediaStreamSource(stream)\n this.sourceNode = source\n const workletNode = new AudioWorkletNode(\n audioContext,\n \"audio-capture-processor\",\n )\n this.workletNode = workletNode\n const silentGainNode = audioContext.createGain()\n silentGainNode.gain.value = 0\n this.silentGainNode = silentGainNode\n\n workletNode.port.onmessage = (event) => {\n const { type, data, rms, peak } = event.data\n\n if (type === \"audio\") {\n this.chunks.push(data)\n } else if (type === \"level\" && this.levelCallback) {\n const signalLevel = Math.max(rms ?? 0, (peak ?? 0) * 0.6)\n const targetLevel = normalizeAudioLevel(signalLevel)\n this.visualLevel = smoothAudioLevel(this.visualLevel, targetLevel)\n this.levelCallback(this.visualLevel)\n } else if (type === \"flush-complete\") {\n this.flushResolve?.()\n this.flushResolve = null\n }\n }\n\n source.connect(workletNode)\n workletNode.connect(silentGainNode)\n silentGainNode.connect(audioContext.destination)\n }\n\n /**\n * Stop recording and return the captured audio as a WAV blob.\n */\n async stop(): Promise<Blob> {\n await this.flushPendingAudio()\n\n // Stop the media stream tracks\n if (this.stream) {\n this.stream.getTracks().forEach((track) => track.stop())\n this.stream = null\n }\n\n // Disconnect and close audio nodes\n if (this.sourceNode) {\n this.sourceNode.disconnect()\n this.sourceNode = null\n }\n\n if (this.workletNode) {\n this.workletNode.disconnect()\n this.workletNode = null\n }\n\n if (this.silentGainNode) {\n this.silentGainNode.disconnect()\n this.silentGainNode = null\n }\n\n if (this.audioContext) {\n await this.audioContext.close()\n this.audioContext = null\n }\n\n // Reset audio level\n this.visualLevel = 0\n this.levelCallback?.(0)\n\n // Encode captured audio as WAV\n const audioData = mergeAudioChunks(this.chunks)\n const wavBlob = encodeWAV(audioData, SAMPLE_RATE)\n\n this.chunks = []\n\n return wavBlob\n }\n\n /**\n * Clean up all resources.\n *\n * The level callback is intentionally preserved so the same service instance\n * can be reused across multiple push-to-talk turns without re-registering\n * the waveform subscription from the client.\n */\n dispose(): void {\n if (this.stream) {\n this.stream.getTracks().forEach((track) => track.stop())\n this.stream = null\n }\n if (this.sourceNode) {\n this.sourceNode.disconnect()\n this.sourceNode = null\n }\n if (this.workletNode) {\n this.workletNode.disconnect()\n this.workletNode = null\n }\n if (this.silentGainNode) {\n this.silentGainNode.disconnect()\n this.silentGainNode = null\n }\n if (this.audioContext) {\n this.audioContext.close()\n this.audioContext = null\n }\n this.chunks = []\n this.visualLevel = 0\n this.levelCallback?.(0)\n this.flushResolve = null\n }\n\n private async flushPendingAudio(): Promise<void> {\n if (!this.workletNode) return\n\n await new Promise<void>((resolve) => {\n const timeoutId = setTimeout(() => {\n this.flushResolve = null\n resolve()\n }, 50)\n\n this.flushResolve = () => {\n clearTimeout(timeoutId)\n resolve()\n }\n\n this.workletNode?.port.postMessage({ type: \"flush\" })\n })\n }\n}\n","import type { VoiceEvent, VoiceState } from \"./types\"\n\n/**\n * State transition table for the voice interaction flow.\n * Maps current state + event type to next state.\n */\nconst transitions: Record<\n VoiceState,\n Partial<Record<VoiceEvent[\"type\"], VoiceState>>\n> = {\n idle: {\n HOTKEY_PRESSED: \"listening\",\n },\n listening: {\n HOTKEY_RELEASED: \"processing\",\n ERROR: \"idle\",\n },\n processing: {\n RESPONSE_STARTED: \"responding\",\n TTS_COMPLETE: \"idle\",\n HOTKEY_PRESSED: \"listening\", // Interruption\n ERROR: \"idle\",\n },\n responding: {\n TTS_COMPLETE: \"idle\",\n HOTKEY_PRESSED: \"listening\", // Interruption\n ERROR: \"idle\",\n },\n}\n\nexport interface StateMachine {\n /** Get current state */\n getState(): VoiceState\n /** Attempt a state transition. Returns true if transition was valid. */\n transition(event: VoiceEvent): boolean\n /** Subscribe to state changes */\n subscribe(listener: () => void): () => void\n /** Reset to idle state */\n reset(): void\n}\n\n/**\n * Create a simple typed state machine for the voice interaction flow.\n *\n * States: idle -> listening -> processing -> responding -> idle\n *\n * Supports interruption: pressing hotkey during processing or responding\n * immediately transitions back to listening.\n */\nexport function createStateMachine(initial: VoiceState = \"idle\"): StateMachine {\n let state = initial\n const listeners = new Set<() => void>()\n\n function notify() {\n listeners.forEach((listener) => listener())\n }\n\n return {\n getState: () => state,\n\n transition: (event: VoiceEvent): boolean => {\n const nextState = transitions[state][event.type]\n if (!nextState) return false\n\n state = nextState\n notify()\n return true\n },\n\n subscribe: (listener: () => void) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n reset: () => {\n state = \"idle\"\n notify()\n },\n }\n}\n","import type { PointToolInput } from \"../../shared/point-tool\"\n\n/**\n * Parsed chunk from AI SDK UI message stream.\n */\nexport type UIStreamChunk =\n | { type: \"text-delta\"; delta: string }\n | { type: \"tool-input-available\"; toolName: string; input: unknown }\n | { type: \"finish\" }\n | { type: \"error\"; errorText: string }\n | { type: \"unknown\" }\n\n/**\n * Parse a single line from the UI message stream.\n * The stream format is SSE with \"data: \" prefix followed by JSON.\n */\nexport function parseUIStreamLine(line: string): UIStreamChunk | null {\n const trimmed = line.trim()\n if (!trimmed) return null\n\n // Handle SSE format: strip \"data: \" prefix\n let jsonStr = trimmed\n if (trimmed.startsWith(\"data: \")) {\n jsonStr = trimmed.slice(6)\n }\n\n // Skip [DONE] marker\n if (jsonStr === \"[DONE]\") return null\n\n try {\n const chunk = JSON.parse(jsonStr) as {\n type: string\n delta?: string\n toolName?: string\n input?: unknown\n errorText?: string\n }\n\n switch (chunk.type) {\n case \"text-delta\":\n return { type: \"text-delta\", delta: chunk.delta ?? \"\" }\n\n case \"tool-input-available\":\n return {\n type: \"tool-input-available\",\n toolName: chunk.toolName ?? \"\",\n input: chunk.input,\n }\n\n case \"finish\":\n return { type: \"finish\" }\n\n case \"error\":\n return { type: \"error\", errorText: chunk.errorText ?? \"Unknown error\" }\n\n default:\n return { type: \"unknown\" }\n }\n } catch {\n return null\n }\n}\n\n/**\n * Check if a tool call is a point tool call with valid input.\n */\nexport function isPointToolCall(\n chunk: UIStreamChunk,\n): chunk is {\n type: \"tool-input-available\"\n toolName: \"point\"\n input: PointToolInput\n} {\n return (\n chunk.type === \"tool-input-available\" &&\n chunk.toolName === \"point\" &&\n chunk.input != null &&\n typeof chunk.input === \"object\" &&\n \"type\" in chunk.input &&\n \"label\" in chunk.input\n )\n}\n","import type { PointToolInput } from \"../../shared/point-tool\"\nimport { isPointToolCall, parseUIStreamLine } from \"./ui-stream-parser\"\n\nconst COMMON_ABBREVIATIONS = [\n \"mr.\",\n \"mrs.\",\n \"ms.\",\n \"dr.\",\n \"prof.\",\n \"sr.\",\n \"jr.\",\n \"e.g.\",\n \"i.e.\",\n]\nconst CLOSING_PUNCTUATION = new Set(['\"', \"'\", \"\\u201D\", \"\\u2019\", \")\", \"]\", \"}\"])\nconst SHORT_SEGMENT_THRESHOLD = 24\n\nfunction isLikelySentenceBoundary(text: string, index: number): boolean {\n const char = text[index]\n if (char === \"!\" || char === \"?\" || char === \"…\" || char === \"\\n\") {\n return true\n }\n\n if (char !== \".\") return false\n\n const previousChar = text[index - 1] ?? \"\"\n const nextChar = text[index + 1] ?? \"\"\n\n if (/\\d/.test(previousChar) && /\\d/.test(nextChar)) {\n return false\n }\n\n const lookback = text.slice(Math.max(0, index - 10), index + 1).toLowerCase()\n if (\n COMMON_ABBREVIATIONS.some((abbreviation) => lookback.endsWith(abbreviation))\n ) {\n return false\n }\n\n return true\n}\n\nfunction findBoundaryEnd(text: string, start: number): number | null {\n for (let index = start; index < text.length; index++) {\n const char = text[index]\n\n if (char === \"\\n\") {\n let end = index + 1\n while (end < text.length && /\\s/.test(text[end] ?? \"\")) {\n end++\n }\n return end\n }\n\n if (!isLikelySentenceBoundary(text, index)) continue\n\n let end = index + 1\n while (end < text.length && CLOSING_PUNCTUATION.has(text[end] ?? \"\")) {\n end++\n }\n\n if (end < text.length) {\n const nextChar = text[end] ?? \"\"\n if (!/\\s/.test(nextChar) && !/[A-Z0-9]/.test(nextChar)) {\n continue\n }\n }\n\n while (end < text.length && /\\s/.test(text[end] ?? \"\")) {\n end++\n }\n\n return end\n }\n\n return null\n}\n\nfunction extractCompletedSegments(text: string): {\n consumedLength: number\n segments: string[]\n} {\n const segments: string[] = []\n let consumedLength = 0\n\n while (consumedLength < text.length) {\n const boundaryEnd = findBoundaryEnd(text, consumedLength)\n if (boundaryEnd === null) break\n\n const segment = text.slice(consumedLength, boundaryEnd).trim()\n if (segment) {\n segments.push(segment)\n }\n\n consumedLength = boundaryEnd\n }\n\n return { consumedLength, segments }\n}\n\nexport interface ProcessedResponseChunk {\n speechSegments: string[]\n visibleText: string\n pointToolCall: PointToolInput | null\n}\n\nexport interface FinalProcessedResponse {\n finalResponseText: string\n speechSegments: string[]\n pointToolCall: PointToolInput | null\n}\n\n/**\n * Processes a streaming AI SDK UI message stream response.\n * Extracts text for display/TTS and captures point tool calls.\n */\nexport class ProgressiveResponseProcessor {\n private consumedTextLength = 0\n private pendingShortSegment = \"\"\n private rawText = \"\"\n private buffer = \"\"\n private pointToolCall: PointToolInput | null = null\n\n /**\n * Push raw stream data and extract text chunks and tool calls.\n * The UI message stream format is newline-delimited JSON.\n */\n push(chunk: string): ProcessedResponseChunk {\n this.buffer += chunk\n const lines = this.buffer.split(\"\\n\")\n\n // Keep incomplete last line in buffer\n this.buffer = lines.pop() ?? \"\"\n\n const newTextParts: string[] = []\n\n for (const line of lines) {\n const parsed = parseUIStreamLine(line)\n if (!parsed) continue\n\n if (parsed.type === \"text-delta\") {\n newTextParts.push(parsed.delta)\n } else if (isPointToolCall(parsed)) {\n // Capture first point tool call only\n if (!this.pointToolCall) {\n this.pointToolCall = parsed.input\n }\n }\n }\n\n // Accumulate new text\n if (newTextParts.length > 0) {\n this.rawText += newTextParts.join(\"\")\n }\n\n // Extract completed sentences for TTS\n const unprocessedText = this.rawText.slice(this.consumedTextLength)\n const { consumedLength, segments } = extractCompletedSegments(unprocessedText)\n this.consumedTextLength += consumedLength\n\n return {\n visibleText: this.rawText,\n speechSegments: this.coalesceSegments(segments),\n pointToolCall: this.pointToolCall,\n }\n }\n\n /**\n * Finalize processing and return any remaining text/tool call.\n */\n finish(): FinalProcessedResponse {\n // Process any remaining buffer\n if (this.buffer) {\n const parsed = parseUIStreamLine(this.buffer)\n if (parsed?.type === \"text-delta\") {\n this.rawText += parsed.delta\n } else if (parsed && isPointToolCall(parsed) && !this.pointToolCall) {\n this.pointToolCall = parsed.input\n }\n this.buffer = \"\"\n }\n\n const trailingText = this.rawText.slice(this.consumedTextLength).trim()\n const finalSegmentParts = [this.pendingShortSegment, trailingText].filter(\n Boolean,\n )\n\n this.pendingShortSegment = \"\"\n\n return {\n finalResponseText: this.rawText.trim(),\n speechSegments: finalSegmentParts.length\n ? [finalSegmentParts.join(\" \").trim()]\n : [],\n pointToolCall: this.pointToolCall,\n }\n }\n\n private coalesceSegments(segments: string[]): string[] {\n const speechSegments: string[] = []\n\n for (const segment of segments) {\n const normalizedSegment = segment.trim()\n if (!normalizedSegment) continue\n\n const candidate = this.pendingShortSegment\n ? `${this.pendingShortSegment} ${normalizedSegment}`\n : normalizedSegment\n\n if (candidate.length < SHORT_SEGMENT_THRESHOLD) {\n this.pendingShortSegment = candidate\n continue\n }\n\n this.pendingShortSegment = \"\"\n speechSegments.push(candidate)\n }\n\n return speechSegments\n }\n}\n","import type { PointToolInput } from \"../shared/point-tool\"\nimport { $audioLevel, $conversationHistory, $isEnabled } from \"./atoms\"\nimport { AudioPlaybackService } from \"./services/audio-playback\"\nimport { BrowserSpeechService } from \"./services/browser-speech\"\nimport { LiveTranscriptionService } from \"./services/live-transcription\"\nimport { PointerController } from \"./services/pointer-controller\"\nimport { ScreenCaptureService } from \"./services/screen-capture\"\nimport {\n type SpeechPlaybackTask,\n TTSPlaybackQueue,\n} from \"./services/tts-playback-queue\"\nimport { VoiceCaptureService } from \"./services/voice-capture\"\nimport { createStateMachine, type StateMachine } from \"./state-machine\"\nimport type {\n AnnotatedScreenshotResult,\n AudioPlaybackPort,\n BrowserSpeechPort,\n ConversationMessage,\n CursorBuddyClientOptions,\n CursorBuddyMediaMode,\n CursorBuddyServices,\n CursorBuddySnapshot,\n LiveTranscriptionPort,\n PointerControllerPort,\n PointingTarget,\n ScreenCapturePort,\n VoiceCapturePort,\n} from \"./types\"\nimport { resolveMarkerToCoordinates } from \"./utils/elements\"\nimport { toError } from \"./utils/error\"\nimport { ProgressiveResponseProcessor } from \"./utils/response-processor\"\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\nasync function readErrorMessage(\n response: Response,\n fallbackMessage: string,\n): Promise<string> {\n try {\n // Prefer structured JSON errors from our handlers, but degrade gracefully\n // to plain text if the route returns a different payload.\n const contentType = response.headers.get(\"Content-Type\") ?? \"\"\n\n if (contentType.includes(\"application/json\")) {\n const body = (await response.json()) as { error?: string }\n if (body?.error) return body.error\n }\n\n const text = await response.text()\n if (text) return text\n } catch {\n // Fall back to the generic message when the response body cannot be read.\n }\n\n return fallbackMessage\n}\n\n/**\n * Map coordinate-based pointing from screenshot space to viewport space.\n */\nfunction mapCoordinatesToViewport(\n x: number,\n y: number,\n screenshot: AnnotatedScreenshotResult,\n): { x: number; y: number } {\n if (screenshot.width <= 0 || screenshot.height <= 0) {\n return { x, y }\n }\n\n const scaleX = screenshot.viewportWidth / screenshot.width\n const scaleY = screenshot.viewportHeight / screenshot.height\n\n return {\n x: clamp(\n Math.round(x * scaleX),\n 0,\n Math.max(screenshot.viewportWidth - 1, 0),\n ),\n y: clamp(\n Math.round(y * scaleY),\n 0,\n Math.max(screenshot.viewportHeight - 1, 0),\n ),\n }\n}\n\nexport type { CursorBuddyServices } from \"./types\"\n\n/**\n * Framework-agnostic client for cursor buddy voice interactions.\n *\n * Manages the complete voice interaction flow:\n * idle -> listening -> processing -> responding -> idle\n *\n * Supports interruption: pressing hotkey during any state aborts\n * in-flight work and immediately transitions to listening.\n */\nexport class CursorBuddyClient {\n private endpoint: string\n private options: CursorBuddyClientOptions\n\n // Services\n private voiceCapture: VoiceCapturePort\n private audioPlayback: AudioPlaybackPort\n private browserSpeech: BrowserSpeechPort\n private liveTranscription: LiveTranscriptionPort\n private screenCapture: ScreenCapturePort\n private pointerController: PointerControllerPort\n private stateMachine: StateMachine\n\n // State\n private liveTranscript = \"\"\n private transcript = \"\"\n private response = \"\"\n private error: Error | null = null\n private abortController: AbortController | null = null\n private historyCommittedForTurn = false\n private speechProviderForTurn: \"browser\" | \"server\" | null = null\n private screenshotPromise: Promise<AnnotatedScreenshotResult> | null = null\n\n // Cached snapshot for useSyncExternalStore (must be referentially stable)\n private cachedSnapshot: CursorBuddySnapshot\n\n // Subscriptions\n private listeners = new Set<() => void>()\n\n constructor(\n endpoint: string,\n options: CursorBuddyClientOptions = {},\n services: CursorBuddyServices = {},\n ) {\n this.endpoint = endpoint\n this.options = options\n\n // Initialize services (allow injection for testing)\n this.voiceCapture = services.voiceCapture ?? new VoiceCaptureService()\n this.audioPlayback = services.audioPlayback ?? new AudioPlaybackService()\n this.browserSpeech = services.browserSpeech ?? new BrowserSpeechService()\n this.liveTranscription =\n services.liveTranscription ?? new LiveTranscriptionService()\n this.screenCapture = services.screenCapture ?? new ScreenCaptureService()\n this.pointerController =\n services.pointerController ?? new PointerController()\n this.stateMachine = createStateMachine()\n\n // Initialize cached snapshot\n this.cachedSnapshot = this.buildSnapshot()\n\n // Wire up audio level to atom\n this.voiceCapture.onLevel((level) => $audioLevel.set(level))\n this.liveTranscription.onPartial((text) => {\n if (this.liveTranscript === text) return\n this.liveTranscript = text\n this.notify()\n })\n\n // Wire up state machine changes\n this.stateMachine.subscribe(() => {\n this.options.onStateChange?.(this.stateMachine.getState())\n this.notify()\n })\n\n // Wire up pointer controller\n this.pointerController.subscribe(() => this.notify())\n }\n\n // === Public API ===\n\n /**\n * Start listening for voice input.\n * Aborts any in-flight work from previous session.\n */\n startListening(): void {\n // 1. Abort previous session synchronously\n this.abort()\n\n // 2. Clear UI state immediately\n this.liveTranscript = \"\"\n this.transcript = \"\"\n this.response = \"\"\n this.error = null\n this.historyCommittedForTurn = false\n this.speechProviderForTurn = null\n this.pointerController.release()\n\n // 3. Transition state\n this.stateMachine.transition({ type: \"HOTKEY_PRESSED\" })\n this.notify()\n\n // 4. Start mic (async, errors go to error state)\n this.abortController = new AbortController()\n const signal = this.abortController.signal\n\n // 5. Screenshot is captured in parallel with voice input to reduce latency\n this.screenshotPromise = this.screenCapture.captureAnnotated()\n\n // 6. Start mic (async, errors go to error state)\n this.beginListeningSession(signal).catch((error) => {\n if (signal.aborted) return\n\n this.voiceCapture.dispose()\n this.liveTranscription.dispose()\n this.handleError(toError(error, \"Failed to start listening\"))\n })\n }\n\n /**\n * Stop listening and process the voice input.\n */\n async stopListening(): Promise<void> {\n if (this.stateMachine.getState() !== \"listening\") return\n\n this.stateMachine.transition({ type: \"HOTKEY_RELEASED\" })\n const signal = this.abortController?.signal\n let turnFailure: Error | null = null\n\n const failTurn = (error: Error) => {\n if (turnFailure || signal?.aborted) return\n\n turnFailure = error\n this.audioPlayback.stop()\n this.browserSpeech.stop()\n this.abortController?.abort()\n }\n\n try {\n const [audioBlob, browserTranscript] = await Promise.all([\n this.voiceCapture.stop(),\n this.stopLiveTranscription(),\n ])\n\n let screenshot: AnnotatedScreenshotResult\n try {\n if (!this.screenshotPromise) {\n throw new Error(\"Screenshot was not started\")\n }\n screenshot = await this.screenshotPromise\n } catch (screenshotError) {\n const errorMessage =\n screenshotError instanceof Error\n ? `Failed to capture screenshot: ${screenshotError.message}`\n : \"Failed to capture screenshot\"\n throw new Error(errorMessage)\n }\n\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n // Resolve transcript from browser or server fallback\n const transcript = await this.resolveTranscript(\n browserTranscript,\n audioBlob,\n signal,\n )\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n this.liveTranscript = \"\"\n this.transcript = transcript\n this.options.onTranscript?.(transcript)\n this.notify()\n\n this.prepareSpeechMode()\n\n // Chat stream + progressive sentence TTS\n const { cleanResponse, pointToolCall, playbackQueue } =\n await this.chatAndSpeak(transcript, screenshot, signal, {\n onFailure: failTurn,\n onPlaybackStart: () => {\n this.stateMachine.transition({ type: \"RESPONSE_STARTED\" })\n },\n })\n\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n this.options.onResponse?.(cleanResponse)\n\n // Resolve pointing target from tool call (marker-based or coordinate-based)\n let pointTarget: PointingTarget | null = null\n\n if (pointToolCall) {\n if (pointToolCall.type === \"marker\") {\n // Resolve marker ID to element coordinates\n const coords = resolveMarkerToCoordinates(\n screenshot.markerMap,\n pointToolCall.markerId!,\n )\n if (coords) {\n pointTarget = { ...coords, label: pointToolCall.label }\n }\n } else {\n // Map coordinates from screenshot space to viewport space\n const coords = mapCoordinatesToViewport(\n pointToolCall.x!,\n pointToolCall.y!,\n screenshot,\n )\n pointTarget = { ...coords, label: pointToolCall.label }\n }\n }\n\n // Point if we have a valid target\n if (pointTarget) {\n this.options.onPoint?.(pointTarget)\n this.pointerController.pointAt(pointTarget)\n }\n\n await playbackQueue.waitForCompletion()\n\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n // Update history only after audio playback succeeds for the full turn\n const history = $conversationHistory.get()\n const newHistory: ConversationMessage[] = [\n ...history,\n { role: \"user\", content: transcript },\n { role: \"assistant\", content: cleanResponse },\n ]\n $conversationHistory.set(newHistory)\n this.historyCommittedForTurn = true\n\n this.stateMachine.transition({ type: \"TTS_COMPLETE\" })\n } catch (err) {\n if (turnFailure) {\n this.handleError(turnFailure)\n return\n }\n // Interruption is not an error\n if (signal?.aborted) return\n this.handleError(toError(err))\n }\n }\n\n /**\n * Enable or disable the buddy.\n */\n setEnabled(enabled: boolean): void {\n $isEnabled.set(enabled)\n this.notify()\n }\n\n /**\n * Manually point at coordinates.\n */\n pointAt(x: number, y: number, label: string): void {\n this.pointerController.pointAt({ x, y, label })\n }\n\n /**\n * Dismiss the current pointing target.\n */\n dismissPointing(): void {\n this.pointerController.release()\n }\n\n /**\n * Reset to idle state and stop any in-progress work.\n */\n reset(): void {\n this.abort()\n this.liveTranscript = \"\"\n this.transcript = \"\"\n this.response = \"\"\n this.error = null\n this.historyCommittedForTurn = false\n this.pointerController.release()\n this.stateMachine.reset()\n this.notify()\n }\n\n /**\n * Update buddy position to follow cursor.\n * Call this on cursor position changes.\n */\n updateCursorPosition(): void {\n this.pointerController.updateFollowPosition()\n }\n\n /**\n * Subscribe to state changes.\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /**\n * Get current state snapshot for React's useSyncExternalStore.\n * Returns a cached object to ensure referential stability.\n */\n getSnapshot(): CursorBuddySnapshot {\n return this.cachedSnapshot\n }\n\n /**\n * Build a new snapshot object.\n */\n private buildSnapshot(): CursorBuddySnapshot {\n return {\n state: this.stateMachine.getState(),\n liveTranscript: this.liveTranscript,\n transcript: this.transcript,\n response: this.response,\n error: this.error,\n isPointing: this.pointerController.isPointing(),\n isEnabled: $isEnabled.get(),\n }\n }\n\n // === Private Methods ===\n\n private abort(): void {\n // Commit partial turn to history if interrupted mid-turn\n this.commitPartialHistory()\n\n this.abortController?.abort()\n this.abortController = null\n this.screenshotPromise = null\n this.voiceCapture.dispose()\n this.liveTranscription.dispose()\n this.audioPlayback.stop()\n this.browserSpeech.stop()\n this.speechProviderForTurn = null\n // Reset audio level on abort\n $audioLevel.set(0)\n }\n\n /**\n * Commit partial turn to history when interrupted.\n * Only commits if we have both transcript and response,\n * and haven't already committed for this turn.\n */\n private commitPartialHistory(): void {\n if (this.historyCommittedForTurn) return\n if (!this.transcript || !this.response) return\n\n const history = $conversationHistory.get()\n const newHistory: ConversationMessage[] = [\n ...history,\n { role: \"user\", content: this.transcript },\n { role: \"assistant\", content: this.response }, // already stripped of POINT tags\n ]\n $conversationHistory.set(newHistory)\n this.historyCommittedForTurn = true\n }\n\n private async transcribe(blob: Blob, signal?: AbortSignal): Promise<string> {\n const formData = new FormData()\n formData.append(\"audio\", blob, \"recording.wav\")\n\n const response = await fetch(`${this.endpoint}/transcribe`, {\n method: \"POST\",\n body: formData,\n signal,\n })\n\n if (!response.ok) {\n throw new Error(await readErrorMessage(response, \"Transcription failed\"))\n }\n\n const { text } = await response.json()\n return text\n }\n\n /**\n * Stream the chat response, keep the visible text updated, and feed complete\n * speech segments into the TTS queue as soon as they are ready.\n */\n private async chatAndSpeak(\n transcript: string,\n screenshot: AnnotatedScreenshotResult,\n signal: AbortSignal | undefined,\n options: {\n onFailure: (error: Error) => void\n onPlaybackStart: () => void\n },\n ): Promise<{\n cleanResponse: string\n pointToolCall: PointToolInput | null\n playbackQueue: TTSPlaybackQueue\n }> {\n const history = $conversationHistory.get()\n\n const response = await fetch(`${this.endpoint}/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n screenshot: screenshot.imageData,\n capture: {\n width: screenshot.width,\n height: screenshot.height,\n },\n transcript,\n history,\n markerContext: screenshot.markerContext,\n }),\n signal,\n })\n\n if (!response.ok) {\n throw new Error(\"Chat request failed\")\n }\n\n const reader = response.body?.getReader()\n if (!reader) throw new Error(\"No response body\")\n\n const decoder = new TextDecoder()\n const responseProcessor = new ProgressiveResponseProcessor()\n const playbackQueue = new TTSPlaybackQueue({\n onError: options.onFailure,\n onPlaybackStart: options.onPlaybackStart,\n prepare: (text, currentSignal) =>\n this.prepareSpeechSegment(text, currentSignal),\n signal,\n })\n const shouldStreamSpeech = this.isSpeechStreamingEnabled()\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n const chunk = decoder.decode(value, { stream: true })\n const { speechSegments, visibleText } = responseProcessor.push(chunk)\n\n if (shouldStreamSpeech) {\n // Queue speech as early as possible, but keep playback ordered so the\n // spoken response stays aligned with the streamed text.\n for (const speechSegment of speechSegments) {\n playbackQueue.enqueue(speechSegment)\n }\n }\n\n this.updateResponse(visibleText)\n }\n\n const trailingChunk = decoder.decode()\n if (trailingChunk) {\n const { speechSegments, visibleText } =\n responseProcessor.push(trailingChunk)\n\n if (shouldStreamSpeech) {\n for (const speechSegment of speechSegments) {\n playbackQueue.enqueue(speechSegment)\n }\n }\n\n this.updateResponse(visibleText)\n }\n\n const finalizedResponse = responseProcessor.finish()\n\n if (shouldStreamSpeech) {\n for (const speechSegment of finalizedResponse.speechSegments) {\n playbackQueue.enqueue(speechSegment)\n }\n } else {\n playbackQueue.enqueue(finalizedResponse.finalResponseText)\n }\n\n this.updateResponse(finalizedResponse.finalResponseText)\n\n return {\n cleanResponse: finalizedResponse.finalResponseText,\n pointToolCall: finalizedResponse.pointToolCall,\n playbackQueue,\n }\n }\n\n /**\n * Request server-side TTS audio for one text segment.\n */\n private async synthesizeSpeech(\n text: string,\n signal?: AbortSignal,\n ): Promise<Blob> {\n const response = await fetch(`${this.endpoint}/tts`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text }),\n signal,\n })\n\n if (!response.ok) {\n throw new Error(await readErrorMessage(response, \"TTS request failed\"))\n }\n\n return response.blob()\n }\n\n /**\n * Resolve the initial speech provider for this turn.\n *\n * Decision tree:\n * 1. In `server` mode, always synthesize on the server.\n * 2. In `browser` mode, require browser speech support up front.\n * 3. In `auto` mode, prefer browser speech when available and keep that\n * choice cached so later segments stay on the same provider unless a\n * browser failure forces a one-way fallback to the server.\n */\n private prepareSpeechMode(): void {\n const speechMode = this.getSpeechMode()\n\n if (speechMode === \"browser\" && !this.browserSpeech.isAvailable()) {\n throw new Error(\"Browser speech is not supported\")\n }\n\n if (speechMode === \"server\") {\n this.speechProviderForTurn = \"server\"\n return\n }\n\n if (speechMode === \"browser\") {\n this.speechProviderForTurn = \"browser\"\n return\n }\n\n this.speechProviderForTurn = this.browserSpeech.isAvailable()\n ? \"browser\"\n : \"server\"\n }\n\n /**\n * Prepare a playback task for one text segment.\n *\n * The queue calls this eagerly so server synthesis can overlap with the\n * currently playing segment, but the returned task is still executed in the\n * original enqueue order.\n */\n private async prepareSpeechSegment(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n switch (this.getSpeechMode()) {\n case \"server\":\n return this.prepareServerSpeechTask(text, signal)\n case \"browser\":\n return this.prepareBrowserSpeechTask(text, signal)\n default:\n return this.prepareAutoSpeechTask(text, signal)\n }\n }\n\n /**\n * Synthesize server audio immediately and return a playback task that reuses\n * the prepared blob later.\n */\n private async prepareServerSpeechTask(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n const blob = await this.synthesizeSpeech(text, signal)\n\n return () => this.audioPlayback.play(blob, signal)\n }\n\n /**\n * Return a browser playback task for one text segment.\n */\n private async prepareBrowserSpeechTask(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n return () => this.browserSpeech.speak(text, signal)\n }\n\n /**\n * Prepare a playback task for `auto` mode.\n *\n * We prefer the browser for low latency, but if browser speech fails for any\n * segment we permanently switch the remainder of the turn to server TTS so\n * later segments do not keep retrying the failing browser path.\n */\n private async prepareAutoSpeechTask(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n if (this.getAutoSpeechProvider() === \"server\") {\n return this.prepareServerSpeechTask(text, signal)\n }\n\n return async () => {\n // Another segment may already have forced a fallback before this one\n // reaches playback, so re-check the cached provider decision here.\n if (this.getAutoSpeechProvider() === \"server\") {\n const fallbackPlayback = await this.prepareServerSpeechTask(\n text,\n signal,\n )\n await fallbackPlayback()\n return\n }\n\n try {\n await this.browserSpeech.speak(text, signal)\n } catch (error) {\n if (signal?.aborted) return\n\n // Browser speech failed mid-turn. Flip future segments to the server\n // and replay the current segment there so the turn still completes.\n this.speechProviderForTurn = \"server\"\n const fallbackPlayback = await this.prepareServerSpeechTask(\n text,\n signal,\n )\n await fallbackPlayback()\n }\n }\n }\n\n /**\n * Read the current provider choice for `auto` mode, lazily defaulting to the\n * browser when supported and the server otherwise.\n */\n private getAutoSpeechProvider(): \"browser\" | \"server\" {\n if (this.speechProviderForTurn) {\n return this.speechProviderForTurn\n }\n\n this.speechProviderForTurn = this.browserSpeech.isAvailable()\n ? \"browser\"\n : \"server\"\n\n return this.speechProviderForTurn\n }\n\n private handleError(err: Error): void {\n this.liveTranscript = \"\"\n this.error = err\n this.stateMachine.transition({ type: \"ERROR\", error: err })\n this.options.onError?.(err)\n this.notify()\n }\n\n /**\n * Resolve the effective transcription mode for the current client.\n */\n private getTranscriptionMode(): CursorBuddyMediaMode {\n return this.options.transcription?.mode ?? \"auto\"\n }\n\n /**\n * Resolve the effective speech mode for the current client.\n */\n private getSpeechMode(): CursorBuddyMediaMode {\n return this.options.speech?.mode ?? \"server\"\n }\n\n /**\n * Decide whether speech should start before the full chat response is ready.\n */\n private isSpeechStreamingEnabled(): boolean {\n return this.options.speech?.allowStreaming ?? false\n }\n\n /**\n * Decide whether this turn should attempt browser speech recognition.\n */\n private shouldAttemptBrowserTranscription(): boolean {\n return this.getTranscriptionMode() !== \"server\"\n }\n\n /**\n * Decide whether browser speech recognition is mandatory for this turn.\n */\n private isBrowserTranscriptionRequired(): boolean {\n return this.getTranscriptionMode() === \"browser\"\n }\n\n /**\n * Start the recorder and browser speech recognition together.\n *\n * The recorder always runs so we keep waveform updates and preserve a raw\n * audio backup for server fallback in `auto` mode.\n */\n private async beginListeningSession(signal: AbortSignal): Promise<void> {\n const shouldAttemptBrowser = this.shouldAttemptBrowserTranscription()\n const isBrowserTranscriptionAvailable =\n shouldAttemptBrowser && this.liveTranscription.isAvailable()\n\n if (shouldAttemptBrowser && !isBrowserTranscriptionAvailable) {\n if (this.isBrowserTranscriptionRequired()) {\n throw new Error(\"Browser transcription is not supported\")\n }\n }\n\n const [voiceCaptureResult, browserTranscriptionResult] =\n await Promise.allSettled([\n this.voiceCapture.start(),\n isBrowserTranscriptionAvailable\n ? this.liveTranscription.start()\n : Promise.resolve(undefined),\n ])\n\n if (signal.aborted) return\n\n if (voiceCaptureResult.status === \"rejected\") {\n throw toError(voiceCaptureResult.reason, \"Failed to start microphone\")\n }\n\n // In browser-only mode, a browser STT startup failure should fail the turn.\n // In auto mode we silently keep the recorder alive for server fallback.\n if (\n browserTranscriptionResult.status === \"rejected\" &&\n this.isBrowserTranscriptionRequired()\n ) {\n throw toError(\n browserTranscriptionResult.reason,\n \"Browser transcription failed to start\",\n )\n }\n\n if (browserTranscriptionResult.status === \"rejected\") {\n this.liveTranscription.dispose()\n }\n }\n\n /**\n * Stop browser speech recognition and return the best final transcript it\n * produced for this turn.\n */\n private async stopLiveTranscription(): Promise<string> {\n if (\n !this.shouldAttemptBrowserTranscription() ||\n !this.liveTranscription.isAvailable()\n ) {\n return \"\"\n }\n\n try {\n return await this.liveTranscription.stop()\n } catch (error) {\n // Browser mode should surface the recognition error directly.\n // Auto mode falls back to the recorded audio instead.\n if (this.isBrowserTranscriptionRequired()) {\n throw toError(error, \"Browser transcription failed\")\n }\n\n return \"\"\n }\n }\n\n /**\n * Choose the transcript that should drive the turn.\n *\n * Decision tree:\n * 1. Use the browser transcript when it is available.\n * 2. In browser-only mode, fail if the browser produced nothing usable.\n * 3. In auto/server modes, fall back to the recorded audio upload.\n */\n private async resolveTranscript(\n browserTranscript: string,\n audioBlob: Blob,\n signal?: AbortSignal,\n ): Promise<string> {\n const normalizedBrowserTranscript = browserTranscript.trim()\n if (normalizedBrowserTranscript) {\n return normalizedBrowserTranscript\n }\n\n if (this.getTranscriptionMode() === \"browser\") {\n throw new Error(\n \"Browser transcription did not produce a final transcript\",\n )\n }\n\n return this.transcribe(audioBlob, signal)\n }\n\n private updateResponse(text: string): void {\n if (this.response === text) return\n\n this.response = text\n this.notify()\n }\n\n private notify(): void {\n // Update cached snapshot before notifying (required for useSyncExternalStore)\n this.cachedSnapshot = this.buildSnapshot()\n this.listeners.forEach((listener) => listener())\n }\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as CursorBuddyClient } from "./client-CevxN9EX.mjs";
2
- import { t as pointTool } from "./point-tool-DtHgq6gQ.mjs";
1
+ import { t as CursorBuddyClient } from "./client-CSVSY-KV.mjs";
2
+ import { t as pointTool } from "./point-tool-Cv39qylv.mjs";
3
3
  export { CursorBuddyClient, pointTool };
@@ -5,8 +5,8 @@ const pointTool = tool({
5
5
  inputSchema: z.object({
6
6
  type: z.enum(["marker", "coordinates"]).describe("How to point. Use 'marker' for interactive elements that have a marker. Use 'coordinates' only for visible non-interactive content without a marker."),
7
7
  markerId: z.number().int().min(0).optional().describe("Required when type is 'marker'. The marker ID of the interactive element to point at."),
8
- x: z.number().int().min(0).optional().describe("Required when type is 'coordinates'. The horizontal pixel coordinate of the center of the target area."),
9
- y: z.number().int().min(0).optional().describe("Required when type is 'coordinates'. The vertical pixel coordinate of the center of the target area."),
8
+ x: z.number().int().min(0).optional().describe("Required when type is 'coordinates'. The horizontal pixel coordinate from the left edge of the screenshot (0 = leftmost). Must be within the screenshot width."),
9
+ y: z.number().int().min(0).optional().describe("Required when type is 'coordinates'. The vertical pixel coordinate from the top edge of the screenshot (0 = topmost). Must be within the screenshot height."),
10
10
  label: z.string().min(1).max(24).describe("A very short label for the pointer bubble, ideally 2 to 4 words.")
11
11
  }).superRefine((value, ctx) => {
12
12
  if (value.type === "marker") {
@@ -51,4 +51,4 @@ const pointTool = tool({
51
51
  //#endregion
52
52
  export { pointTool as t };
53
53
 
54
- //# sourceMappingURL=point-tool-DtHgq6gQ.mjs.map
54
+ //# sourceMappingURL=point-tool-Cv39qylv.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"point-tool-Cv39qylv.mjs","names":[],"sources":["../src/shared/point-tool.ts"],"sourcesContent":["import { tool } from \"ai\"\nimport { z } from \"zod\"\n\nexport const pointToolInputSchema = z\n .object({\n type: z\n .enum([\"marker\", \"coordinates\"])\n .describe(\n \"How to point. Use 'marker' for interactive elements that have a marker. Use 'coordinates' only for visible non-interactive content without a marker.\",\n ),\n\n markerId: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n \"Required when type is 'marker'. The marker ID of the interactive element to point at.\",\n ),\n\n x: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n \"Required when type is 'coordinates'. The horizontal pixel coordinate from the left edge of the screenshot (0 = leftmost). Must be within the screenshot width.\",\n ),\n\n y: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n \"Required when type is 'coordinates'. The vertical pixel coordinate from the top edge of the screenshot (0 = topmost). Must be within the screenshot height.\",\n ),\n\n label: z\n .string()\n .min(1)\n .max(24)\n .describe(\n \"A very short label for the pointer bubble, ideally 2 to 4 words.\",\n ),\n })\n .superRefine((value, ctx) => {\n if (value.type === \"marker\") {\n if (value.markerId == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"markerId\"],\n message: \"markerId is required when type is 'marker'.\",\n })\n }\n\n if (value.x != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"x\"],\n message: \"x must not be provided when type is 'marker'.\",\n })\n }\n\n if (value.y != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"y\"],\n message: \"y must not be provided when type is 'marker'.\",\n })\n }\n }\n\n if (value.type === \"coordinates\") {\n if (value.x == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"x\"],\n message: \"x is required when type is 'coordinates'.\",\n })\n }\n\n if (value.y == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"y\"],\n message: \"y is required when type is 'coordinates'.\",\n })\n }\n\n if (value.markerId != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"markerId\"],\n message: \"markerId must not be provided when type is 'coordinates'.\",\n })\n }\n }\n })\n\nexport type PointToolInput = z.infer<typeof pointToolInputSchema>\n\nexport const pointTool = tool({\n description:\n \"Visually point at something on the user's screen. \" +\n \"Use this tool when the user asks you to locate, indicate, highlight, or show a specific visible target on screen. \" +\n \"Prefer type 'marker' for interactive elements that have a marker. \" +\n \"Use type 'coordinates' only for visible non-interactive content without a marker. \" +\n \"Do not describe a pointing action in plain text instead of calling this tool. \" +\n \"Call this tool at most once per response, and only after your spoken reply.\",\n\n inputSchema: pointToolInputSchema,\n\n execute: async (params) => {\n return `Pointed at \"${params.label}\" on the user's screen.`\n },\n})\n"],"mappings":";;AAsGA,MAAa,YAAY,KAAK;CAC5B,aACE;CAOF,aA5GkC,EACjC,OAAO;EACN,MAAM,EACH,KAAK,CAAC,UAAU,cAAc,CAAC,CAC/B,SACC,uJACD;EAEH,UAAU,EACP,QAAQ,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SACC,wFACD;EAEH,GAAG,EACA,QAAQ,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SACC,iKACD;EAEH,GAAG,EACA,QAAQ,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SACC,8JACD;EAEH,OAAO,EACJ,QAAQ,CACR,IAAI,EAAE,CACN,IAAI,GAAG,CACP,SACC,mEACD;EACJ,CAAC,CACD,aAAa,OAAO,QAAQ;AAC3B,MAAI,MAAM,SAAS,UAAU;AAC3B,OAAI,MAAM,YAAY,KACpB,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,WAAW;IAClB,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;;AAIN,MAAI,MAAM,SAAS,eAAe;AAChC,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,YAAY,KACpB,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,WAAW;IAClB,SAAS;IACV,CAAC;;GAGN;CAeF,SAAS,OAAO,WAAW;AACzB,SAAO,eAAe,OAAO,MAAM;;CAEtC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { a as $buddyScale, i as $buddyRotation, n as $audioLevel, o as $cursorPosition, r as $buddyPosition, s as $pointingTarget, t as CursorBuddyClient } from "../client-CevxN9EX.mjs";
2
+ import { a as $buddyScale, i as $buddyRotation, n as $audioLevel, o as $cursorPosition, r as $buddyPosition, s as $pointingTarget, t as CursorBuddyClient } from "../client-CSVSY-KV.mjs";
3
3
  import { useStore } from "@nanostores/react";
4
4
  import { createContext, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,4 +1,4 @@
1
- import { t as CursorBuddyHandler } from "../../types-COQKMo5C.mjs";
1
+ import { t as CursorBuddyHandler } from "../../types-BJfkApb_.mjs";
2
2
 
3
3
  //#region src/server/adapters/next.d.ts
4
4
  /**
@@ -10,11 +10,10 @@ import { t as CursorBuddyHandler } from "../../types-COQKMo5C.mjs";
10
10
  * import { toNextJsHandler } from "cursor-buddy/server/next"
11
11
  * import { cursorBuddy } from "@/lib/cursor-buddy"
12
12
  *
13
- * export const { GET, POST } = toNextJsHandler(cursorBuddy)
13
+ * export const { POST } = toNextJsHandler(cursorBuddy)
14
14
  * ```
15
15
  */
16
16
  declare function toNextJsHandler(cursorBuddy: CursorBuddyHandler): {
17
- GET: (request: Request) => Promise<Response>;
18
17
  POST: (request: Request) => Promise<Response>;
19
18
  };
20
19
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"next.d.mts","names":[],"sources":["../../../src/server/adapters/next.ts"],"mappings":";;;;;AAcA;;;;;;;;;;iBAAgB,eAAA,CAAgB,WAAA,EAAa,kBAAA;iBACjB,OAAA,KAAO,OAAA,CAAA,QAAA;kBAAP,OAAA,KAAO,OAAA,CAAA,QAAA;AAAA"}
1
+ {"version":3,"file":"next.d.mts","names":[],"sources":["../../../src/server/adapters/next.ts"],"mappings":";;;;;AAcA;;;;;;;;;;iBAAgB,eAAA,CAAgB,WAAA,EAAa,kBAAA;kBACjB,OAAA,KAAO,OAAA,CAAA,QAAA;AAAA"}
@@ -8,15 +8,12 @@
8
8
  * import { toNextJsHandler } from "cursor-buddy/server/next"
9
9
  * import { cursorBuddy } from "@/lib/cursor-buddy"
10
10
  *
11
- * export const { GET, POST } = toNextJsHandler(cursorBuddy)
11
+ * export const { POST } = toNextJsHandler(cursorBuddy)
12
12
  * ```
13
13
  */
14
14
  function toNextJsHandler(cursorBuddy) {
15
15
  const handler = (request) => cursorBuddy.handler(request);
16
- return {
17
- GET: handler,
18
- POST: handler
19
- };
16
+ return { POST: handler };
20
17
  }
21
18
  //#endregion
22
19
  export { toNextJsHandler };
@@ -1 +1 @@
1
- {"version":3,"file":"next.mjs","names":[],"sources":["../../../src/server/adapters/next.ts"],"sourcesContent":["import type { CursorBuddyHandler } from \"../types\"\n\n/**\n * Convert a CursorBuddyHandler to Next.js App Router route handlers.\n *\n * @example\n * ```ts\n * // app/api/cursor-buddy/[...path]/route.ts\n * import { toNextJsHandler } from \"cursor-buddy/server/next\"\n * import { cursorBuddy } from \"@/lib/cursor-buddy\"\n *\n * export const { GET, POST } = toNextJsHandler(cursorBuddy)\n * ```\n */\nexport function toNextJsHandler(cursorBuddy: CursorBuddyHandler) {\n const handler = (request: Request) => cursorBuddy.handler(request)\n\n return {\n GET: handler,\n POST: handler,\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,gBAAgB,aAAiC;CAC/D,MAAM,WAAW,YAAqB,YAAY,QAAQ,QAAQ;AAElE,QAAO;EACL,KAAK;EACL,MAAM;EACP"}
1
+ {"version":3,"file":"next.mjs","names":[],"sources":["../../../src/server/adapters/next.ts"],"sourcesContent":["import type { CursorBuddyHandler } from \"../types\"\n\n/**\n * Convert a CursorBuddyHandler to Next.js App Router route handlers.\n *\n * @example\n * ```ts\n * // app/api/cursor-buddy/[...path]/route.ts\n * import { toNextJsHandler } from \"cursor-buddy/server/next\"\n * import { cursorBuddy } from \"@/lib/cursor-buddy\"\n *\n * export const { POST } = toNextJsHandler(cursorBuddy)\n * ```\n */\nexport function toNextJsHandler(cursorBuddy: CursorBuddyHandler) {\n const handler = (request: Request) => cursorBuddy.handler(request)\n\n return {\n POST: handler,\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,gBAAgB,aAAiC;CAC/D,MAAM,WAAW,YAAqB,YAAY,QAAQ,QAAQ;AAElE,QAAO,EACL,MAAM,SACP"}
@@ -1,5 +1,5 @@
1
1
  import { n as pointTool, t as PointToolInput } from "../point-tool-kIviMn1q.mjs";
2
- import { n as CursorBuddyHandlerConfig, t as CursorBuddyHandler } from "../types-COQKMo5C.mjs";
2
+ import { n as CursorBuddyHandlerConfig, t as CursorBuddyHandler } from "../types-BJfkApb_.mjs";
3
3
 
4
4
  //#region src/server/handler.d.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { t as pointTool } from "../point-tool-DtHgq6gQ.mjs";
1
+ import { t as pointTool } from "../point-tool-Cv39qylv.mjs";
2
2
  import { experimental_generateSpeech, experimental_transcribe, streamText } from "ai";
3
3
  //#region src/server/system-prompt.ts
4
4
  const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant that lives inside a web page as a cursor companion.
@@ -220,6 +220,7 @@ async function handleTTS(request, config) {
220
220
  const result = await experimental_generateSpeech({
221
221
  model: config.speechModel,
222
222
  text,
223
+ voice: config?.speechVoice,
223
224
  outputFormat
224
225
  });
225
226
  const audioData = new Uint8Array(result.audio.uint8Array);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["transcribe","generateSpeech"],"sources":["../../src/server/system-prompt.ts","../../src/server/routes/chat.ts","../../src/server/routes/transcribe.ts","../../src/server/routes/tts.ts","../../src/server/handler.ts"],"sourcesContent":["export const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant that lives inside a web page as a cursor companion.\n\nYou can see the user's current screen and hear what they say. Respond conversationally. Your response will be spoken aloud with text-to-speech, so keep it natural, concise, and easy to follow.\n\n## Core behavior\n\n- Speak like a helpful companion, not a robot\n- Keep most responses to 1-3 short sentences\n- Focus on what is visible right now on the user's screen\n- If something is unclear or not visible, say that plainly\n- Do not mention screenshots, overlays, annotations, or internal helper data\n- Do not mention marker numbers to the user\n\n## The point tool\n\nYou have a \\`point\\` tool that can visually indicate something on the user's screen.\n\nUse the \\`point\\` tool when the user is asking you to identify, locate, indicate, highlight, or show something visible on screen.\n\nCommon cases where you should use \\`point\\`:\n- the user asks where something is\n- the user asks what to click\n- the user says things like \"show me\", \"point to it\", \"where is it\", \"which one\", \"what should I click\", or \"highlight that\"\n\nDo not use the \\`point\\` tool when spoken guidance alone is enough and the user is not asking you to identify a specific on-screen target.\n\nExamples where spoken guidance alone may be enough:\n- explaining what a page does\n- answering a general question about what is on screen\n- giving brief next-step advice that does not depend on locating a specific element\n\nIf using the \\`point\\` tool:\n- first give the spoken response\n- then call the tool\n- call it at most once per response\n- point only at the most relevant target\n- never replace the tool call with plain text like \"(point here)\" or \"I’m pointing at it now\"\n\n\nIf the user asks where something is on screen, what to click, or asks you to point something out, you should usually use the point tool rather than only describing it in words.\nDo not say things like \"I can point to it if you want\" when the user already asked where it is. In that case, answer briefly and use the point tool.\n\n## How to point\n\nPrefer marker-based pointing for interactive elements when a marker is available.\nInteractive elements may include buttons, links, inputs, tabs, menus, toggles, and other clickable controls.\n\nUse:\n- \\`type: \"marker\"\\` for interactive elements that have a marker\n- \\`type: \"coordinates\"\\` only for visible non-interactive content without a marker\n\nNever use coordinates for an interactive element if a marker is available.\n\nCoordinates must refer to the center of the target area.\n\nWhen calling the point tool, choose exactly one mode:\n\n- Marker mode:\n - use type \"marker\"\n - provide markerId\n - do not provide x or y\n\n- Coordinates mode:\n - use type \"coordinates\"\n - provide x and y\n - do not provide markerId\n\nNever combine markerId with x or y in the same tool call.\n\n## What to say\n\nWhen the user asks you to point something out:\n- briefly answer in a natural spoken way\n- then use the tool if the request is about locating or indicating something on screen\n\nGood spoken style:\n- \"Click this button right here.\"\n- \"The error message is over here.\"\n- \"This is the field you want.\"\n- \"That setting is in this section.\"\n\nAvoid:\n- mentioning marker IDs\n- mentioning internal tools\n- describing internal reasoning\n- saying you are looking at a screenshot\n\n## If the target is not clear\n\nIf you cannot confidently find the requested thing on screen:\n- say you cannot see it clearly or cannot find it\n- do not point at a random or uncertain target\n\n## Priority\n\nYour first priority is being helpful and correct.\nYour second priority is using the \\`point\\` tool whenever the user is asking you to visually identify a specific thing on screen.\n`\n","import { streamText } from \"ai\"\nimport { pointTool } from \"../../shared/point-tool\"\nimport { DEFAULT_SYSTEM_PROMPT } from \"../system-prompt\"\nimport type { ChatRequestBody, CursorBuddyHandlerConfig } from \"../types\"\n\n/**\n * Handle chat requests: screenshot + transcript → AI SSE stream\n */\nexport async function handleChat(\n request: Request,\n config: CursorBuddyHandlerConfig,\n): Promise<Response> {\n const body = (await request.json()) as ChatRequestBody\n const { screenshot, transcript, history, capture, markerContext } = body\n\n // Resolve system prompt (string or function)\n const systemPrompt =\n typeof config.system === \"function\"\n ? config.system({ defaultPrompt: DEFAULT_SYSTEM_PROMPT })\n : (config.system ?? DEFAULT_SYSTEM_PROMPT)\n\n // Trim history to maxHistory (default 10 exchanges = 20 messages)\n const maxMessages = (config.maxHistory ?? 10) * 2\n const trimmedHistory = history.slice(-maxMessages)\n\n // Build capture context with marker information\n const captureContextParts: string[] = []\n\n if (capture) {\n captureContextParts.push(\n `Screenshot size: ${capture.width}x${capture.height} pixels.`,\n )\n }\n\n if (markerContext) {\n captureContextParts.push(\"\", markerContext)\n }\n\n const captureContext =\n captureContextParts.length > 0 ? captureContextParts.join(\"\\n\") : null\n\n // Build messages array with vision content\n const messages = [\n ...trimmedHistory.map((msg) => ({\n role: msg.role as \"user\" | \"assistant\",\n content: msg.content,\n })),\n {\n role: \"user\" as const,\n content: [\n ...(captureContext\n ? [\n {\n type: \"text\" as const,\n text: captureContext,\n },\n ]\n : []),\n {\n type: \"image\" as const,\n image: screenshot,\n },\n {\n type: \"text\" as const,\n text: transcript,\n },\n ],\n },\n ]\n\n const result = streamText({\n model: config.model,\n system: systemPrompt,\n providerOptions: config?.modelProviderMetadata,\n messages,\n tools: {\n point: pointTool,\n ...config.tools,\n },\n experimental_repairToolCall: async ({ toolCall }) => {\n if (toolCall.toolName !== \"point\") return null\n\n let parsed: unknown\n try {\n parsed = JSON.parse(toolCall.input)\n } catch {\n return null\n }\n\n if (!parsed || typeof parsed !== \"object\") return null\n\n const input = parsed as Record<string, unknown>\n\n if (input.type === \"marker\") {\n const repaired = {\n type: \"marker\",\n markerId: input.markerId,\n label: input.label,\n }\n\n return {\n ...toolCall,\n input: JSON.stringify(repaired),\n }\n }\n\n if (input.type === \"coordinates\") {\n const repaired = {\n type: \"coordinates\",\n x: input.x,\n y: input.y,\n label: input.label,\n }\n\n return {\n ...toolCall,\n input: JSON.stringify(repaired),\n }\n }\n\n return null\n },\n })\n\n return result.toUIMessageStreamResponse()\n}\n","import { experimental_transcribe as transcribe } from \"ai\"\nimport type { CursorBuddyHandlerConfig, TranscribeResponse } from \"../types\"\n\n/**\n * Handle transcription requests: audio file → text\n */\nexport async function handleTranscribe(\n request: Request,\n config: CursorBuddyHandlerConfig,\n): Promise<Response> {\n if (!config.transcriptionModel) {\n return new Response(\n JSON.stringify({\n error:\n \"Server transcription is not configured. Provide a transcriptionModel or use browser transcription only.\",\n }),\n {\n status: 501,\n headers: { \"Content-Type\": \"application/json\" },\n },\n )\n }\n\n const formData = await request.formData()\n const audioFile = formData.get(\"audio\")\n\n if (!audioFile || !(audioFile instanceof File)) {\n return new Response(JSON.stringify({ error: \"No audio file provided\" }), {\n status: 400,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n\n const audioBuffer = await audioFile.arrayBuffer()\n\n const result = await transcribe({\n model: config.transcriptionModel,\n audio: new Uint8Array(audioBuffer),\n })\n\n const response: TranscribeResponse = { text: result.text }\n\n return new Response(JSON.stringify(response), {\n headers: { \"Content-Type\": \"application/json\" },\n })\n}\n","import { experimental_generateSpeech as generateSpeech } from \"ai\"\nimport type { CursorBuddyHandlerConfig, TTSRequestBody } from \"../types\"\n\n/**\n * Handle TTS requests: text → audio\n */\nexport async function handleTTS(\n request: Request,\n config: CursorBuddyHandlerConfig,\n): Promise<Response> {\n if (!config.speechModel) {\n return new Response(\n JSON.stringify({\n error:\n \"Server speech is not configured. Provide a speechModel or use browser speech only.\",\n }),\n {\n status: 501,\n headers: { \"Content-Type\": \"application/json\" },\n },\n )\n }\n\n const outputFormat = \"wav\"\n const body = (await request.json()) as TTSRequestBody\n const { text } = body\n\n if (!text) {\n return new Response(JSON.stringify({ error: \"No text provided\" }), {\n status: 400,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n\n const result = await generateSpeech({\n model: config.speechModel,\n text,\n outputFormat,\n })\n\n // Create a new ArrayBuffer copy to satisfy TypeScript's strict typing\n const audioData = new Uint8Array(result.audio.uint8Array)\n\n return new Response(audioData, {\n headers: {\n \"Content-Type\": \"audio/wav\",\n },\n })\n}\n","import { handleChat } from \"./routes/chat\"\nimport { handleTranscribe } from \"./routes/transcribe\"\nimport { handleTTS } from \"./routes/tts\"\nimport type { CursorBuddyHandler, CursorBuddyHandlerConfig } from \"./types\"\n\n/**\n * Create a cursor buddy request handler.\n *\n * The handler responds to three routes based on the last path segment:\n * - /chat - Screenshot + transcript → AI SSE stream\n * - /transcribe - Audio → text\n * - /tts - Text → audio\n *\n * @example\n * ```ts\n * import { createCursorBuddyHandler } from \"cursor-buddy/server\"\n * import { openai } from \"@ai-sdk/openai\"\n *\n * const cursorBuddy = createCursorBuddyHandler({\n * model: openai(\"gpt-4o\"),\n * speechModel: openai.speech(\"tts-1\"), // optional for browser-only speech\n * transcriptionModel: openai.transcription(\"whisper-1\"),\n * })\n * ```\n */\nexport function createCursorBuddyHandler(\n config: CursorBuddyHandlerConfig,\n): CursorBuddyHandler {\n const handler = async (request: Request): Promise<Response> => {\n const url = new URL(request.url)\n const pathSegments = url.pathname.split(\"/\").filter(Boolean)\n const route = pathSegments[pathSegments.length - 1]\n\n switch (route) {\n case \"chat\":\n return handleChat(request, config)\n\n case \"transcribe\":\n return handleTranscribe(request, config)\n\n case \"tts\":\n return handleTTS(request, config)\n\n default:\n return new Response(\n JSON.stringify({\n error: \"Not found\",\n availableRoutes: [\"/chat\", \"/transcribe\", \"/tts\"],\n }),\n {\n status: 404,\n headers: { \"Content-Type\": \"application/json\" },\n },\n )\n }\n }\n\n return { handler, config }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["transcribe","generateSpeech"],"sources":["../../src/server/system-prompt.ts","../../src/server/routes/chat.ts","../../src/server/routes/transcribe.ts","../../src/server/routes/tts.ts","../../src/server/handler.ts"],"sourcesContent":["export const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant that lives inside a web page as a cursor companion.\n\nYou can see the user's current screen and hear what they say. Respond conversationally. Your response will be spoken aloud with text-to-speech, so keep it natural, concise, and easy to follow.\n\n## Core behavior\n\n- Speak like a helpful companion, not a robot\n- Keep most responses to 1-3 short sentences\n- Focus on what is visible right now on the user's screen\n- If something is unclear or not visible, say that plainly\n- Do not mention screenshots, overlays, annotations, or internal helper data\n- Do not mention marker numbers to the user\n\n## The point tool\n\nYou have a \\`point\\` tool that can visually indicate something on the user's screen.\n\nUse the \\`point\\` tool when the user is asking you to identify, locate, indicate, highlight, or show something visible on screen.\n\nCommon cases where you should use \\`point\\`:\n- the user asks where something is\n- the user asks what to click\n- the user says things like \"show me\", \"point to it\", \"where is it\", \"which one\", \"what should I click\", or \"highlight that\"\n\nDo not use the \\`point\\` tool when spoken guidance alone is enough and the user is not asking you to identify a specific on-screen target.\n\nExamples where spoken guidance alone may be enough:\n- explaining what a page does\n- answering a general question about what is on screen\n- giving brief next-step advice that does not depend on locating a specific element\n\nIf using the \\`point\\` tool:\n- first give the spoken response\n- then call the tool\n- call it at most once per response\n- point only at the most relevant target\n- never replace the tool call with plain text like \"(point here)\" or \"I’m pointing at it now\"\n\n\nIf the user asks where something is on screen, what to click, or asks you to point something out, you should usually use the point tool rather than only describing it in words.\nDo not say things like \"I can point to it if you want\" when the user already asked where it is. In that case, answer briefly and use the point tool.\n\n## How to point\n\nPrefer marker-based pointing for interactive elements when a marker is available.\nInteractive elements may include buttons, links, inputs, tabs, menus, toggles, and other clickable controls.\n\nUse:\n- \\`type: \"marker\"\\` for interactive elements that have a marker\n- \\`type: \"coordinates\"\\` only for visible non-interactive content without a marker\n\nNever use coordinates for an interactive element if a marker is available.\n\nCoordinates must refer to the center of the target area.\n\nWhen calling the point tool, choose exactly one mode:\n\n- Marker mode:\n - use type \"marker\"\n - provide markerId\n - do not provide x or y\n\n- Coordinates mode:\n - use type \"coordinates\"\n - provide x and y\n - do not provide markerId\n\nNever combine markerId with x or y in the same tool call.\n\n## What to say\n\nWhen the user asks you to point something out:\n- briefly answer in a natural spoken way\n- then use the tool if the request is about locating or indicating something on screen\n\nGood spoken style:\n- \"Click this button right here.\"\n- \"The error message is over here.\"\n- \"This is the field you want.\"\n- \"That setting is in this section.\"\n\nAvoid:\n- mentioning marker IDs\n- mentioning internal tools\n- describing internal reasoning\n- saying you are looking at a screenshot\n\n## If the target is not clear\n\nIf you cannot confidently find the requested thing on screen:\n- say you cannot see it clearly or cannot find it\n- do not point at a random or uncertain target\n\n## Priority\n\nYour first priority is being helpful and correct.\nYour second priority is using the \\`point\\` tool whenever the user is asking you to visually identify a specific thing on screen.\n`\n","import { streamText } from \"ai\"\nimport { pointTool } from \"../../shared/point-tool\"\nimport { DEFAULT_SYSTEM_PROMPT } from \"../system-prompt\"\nimport type { ChatRequestBody, CursorBuddyHandlerConfig } from \"../types\"\n\n/**\n * Handle chat requests: screenshot + transcript → AI SSE stream\n */\nexport async function handleChat(\n request: Request,\n config: CursorBuddyHandlerConfig,\n): Promise<Response> {\n const body = (await request.json()) as ChatRequestBody\n const { screenshot, transcript, history, capture, markerContext } = body\n\n // Resolve system prompt (string or function)\n const systemPrompt =\n typeof config.system === \"function\"\n ? config.system({ defaultPrompt: DEFAULT_SYSTEM_PROMPT })\n : (config.system ?? DEFAULT_SYSTEM_PROMPT)\n\n // Trim history to maxHistory (default 10 exchanges = 20 messages)\n const maxMessages = (config.maxHistory ?? 10) * 2\n const trimmedHistory = history.slice(-maxMessages)\n\n // Build capture context with marker information\n const captureContextParts: string[] = []\n\n if (capture) {\n captureContextParts.push(\n `Screenshot size: ${capture.width}x${capture.height} pixels.`,\n )\n }\n\n if (markerContext) {\n captureContextParts.push(\"\", markerContext)\n }\n\n const captureContext =\n captureContextParts.length > 0 ? captureContextParts.join(\"\\n\") : null\n\n // Build messages array with vision content\n const messages = [\n ...trimmedHistory.map((msg) => ({\n role: msg.role as \"user\" | \"assistant\",\n content: msg.content,\n })),\n {\n role: \"user\" as const,\n content: [\n ...(captureContext\n ? [\n {\n type: \"text\" as const,\n text: captureContext,\n },\n ]\n : []),\n {\n type: \"image\" as const,\n image: screenshot,\n },\n {\n type: \"text\" as const,\n text: transcript,\n },\n ],\n },\n ]\n\n const result = streamText({\n model: config.model,\n system: systemPrompt,\n providerOptions: config?.modelProviderMetadata,\n messages,\n tools: {\n point: pointTool,\n ...config.tools,\n },\n experimental_repairToolCall: async ({ toolCall }) => {\n if (toolCall.toolName !== \"point\") return null\n\n let parsed: unknown\n try {\n parsed = JSON.parse(toolCall.input)\n } catch {\n return null\n }\n\n if (!parsed || typeof parsed !== \"object\") return null\n\n const input = parsed as Record<string, unknown>\n\n if (input.type === \"marker\") {\n const repaired = {\n type: \"marker\",\n markerId: input.markerId,\n label: input.label,\n }\n\n return {\n ...toolCall,\n input: JSON.stringify(repaired),\n }\n }\n\n if (input.type === \"coordinates\") {\n const repaired = {\n type: \"coordinates\",\n x: input.x,\n y: input.y,\n label: input.label,\n }\n\n return {\n ...toolCall,\n input: JSON.stringify(repaired),\n }\n }\n\n return null\n },\n })\n\n return result.toUIMessageStreamResponse()\n}\n","import { experimental_transcribe as transcribe } from \"ai\"\nimport type { CursorBuddyHandlerConfig, TranscribeResponse } from \"../types\"\n\n/**\n * Handle transcription requests: audio file → text\n */\nexport async function handleTranscribe(\n request: Request,\n config: CursorBuddyHandlerConfig,\n): Promise<Response> {\n if (!config.transcriptionModel) {\n return new Response(\n JSON.stringify({\n error:\n \"Server transcription is not configured. Provide a transcriptionModel or use browser transcription only.\",\n }),\n {\n status: 501,\n headers: { \"Content-Type\": \"application/json\" },\n },\n )\n }\n\n const formData = await request.formData()\n const audioFile = formData.get(\"audio\")\n\n if (!audioFile || !(audioFile instanceof File)) {\n return new Response(JSON.stringify({ error: \"No audio file provided\" }), {\n status: 400,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n\n const audioBuffer = await audioFile.arrayBuffer()\n\n const result = await transcribe({\n model: config.transcriptionModel,\n audio: new Uint8Array(audioBuffer),\n })\n\n const response: TranscribeResponse = { text: result.text }\n\n return new Response(JSON.stringify(response), {\n headers: { \"Content-Type\": \"application/json\" },\n })\n}\n","import { experimental_generateSpeech as generateSpeech } from \"ai\"\nimport type { CursorBuddyHandlerConfig, TTSRequestBody } from \"../types\"\n\n/**\n * Handle TTS requests: text → audio\n */\nexport async function handleTTS(\n request: Request,\n config: CursorBuddyHandlerConfig,\n): Promise<Response> {\n if (!config.speechModel) {\n return new Response(\n JSON.stringify({\n error:\n \"Server speech is not configured. Provide a speechModel or use browser speech only.\",\n }),\n {\n status: 501,\n headers: { \"Content-Type\": \"application/json\" },\n },\n )\n }\n\n const outputFormat = \"wav\"\n const body = (await request.json()) as TTSRequestBody\n const { text } = body\n\n if (!text) {\n return new Response(JSON.stringify({ error: \"No text provided\" }), {\n status: 400,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n\n const result = await generateSpeech({\n model: config.speechModel,\n text,\n voice: config?.speechVoice,\n outputFormat,\n })\n\n // Create a new ArrayBuffer copy to satisfy TypeScript's strict typing\n const audioData = new Uint8Array(result.audio.uint8Array)\n\n return new Response(audioData, {\n headers: {\n \"Content-Type\": \"audio/wav\",\n },\n })\n}\n","import { handleChat } from \"./routes/chat\"\nimport { handleTranscribe } from \"./routes/transcribe\"\nimport { handleTTS } from \"./routes/tts\"\nimport type { CursorBuddyHandler, CursorBuddyHandlerConfig } from \"./types\"\n\n/**\n * Create a cursor buddy request handler.\n *\n * The handler responds to three routes based on the last path segment:\n * - /chat - Screenshot + transcript → AI SSE stream\n * - /transcribe - Audio → text\n * - /tts - Text → audio\n *\n * @example\n * ```ts\n * import { createCursorBuddyHandler } from \"cursor-buddy/server\"\n * import { openai } from \"@ai-sdk/openai\"\n *\n * const cursorBuddy = createCursorBuddyHandler({\n * model: openai(\"gpt-4o\"),\n * speechModel: openai.speech(\"tts-1\"), // optional for browser-only speech\n * transcriptionModel: openai.transcription(\"whisper-1\"),\n * })\n * ```\n */\nexport function createCursorBuddyHandler(\n config: CursorBuddyHandlerConfig,\n): CursorBuddyHandler {\n const handler = async (request: Request): Promise<Response> => {\n const url = new URL(request.url)\n const pathSegments = url.pathname.split(\"/\").filter(Boolean)\n const route = pathSegments[pathSegments.length - 1]\n\n switch (route) {\n case \"chat\":\n return handleChat(request, config)\n\n case \"transcribe\":\n return handleTranscribe(request, config)\n\n case \"tts\":\n return handleTTS(request, config)\n\n default:\n return new Response(\n JSON.stringify({\n error: \"Not found\",\n availableRoutes: [\"/chat\", \"/transcribe\", \"/tts\"],\n }),\n {\n status: 404,\n headers: { \"Content-Type\": \"application/json\" },\n },\n )\n }\n }\n\n return { handler, config }\n}\n"],"mappings":"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"}
@@ -13,6 +13,7 @@ interface CursorBuddyHandlerConfig {
13
13
  * Optional when clients use browser-only speech.
14
14
  */
15
15
  speechModel?: SpeechModel;
16
+ speechVoice?: string;
16
17
  /**
17
18
  * AI SDK transcription model (e.g., openai.transcription("whisper-1")).
18
19
  * Optional when clients use browser-only transcription.
@@ -41,4 +42,4 @@ interface CursorBuddyHandler {
41
42
  }
42
43
  //#endregion
43
44
  export { CursorBuddyHandlerConfig as n, CursorBuddyHandler as t };
44
- //# sourceMappingURL=types-COQKMo5C.d.mts.map
45
+ //# sourceMappingURL=types-BJfkApb_.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BJfkApb_.d.mts","names":[],"sources":["../src/server/types.ts"],"mappings":";;;;;AAKA;UAAiB,wBAAA;;EAEf,KAAA,EAAO,aAAA;EACP,qBAAA,GAAwB,MAAA;EAMV;;;;EAAd,WAAA,GAAc,WAAA;EACd,WAAA;EARA;;;;EAcA,kBAAA,GAAqB,kBAAA;EAPP;;;;EAad,MAAA,cAAoB,GAAA;IAAO,aAAA;EAAA;EAG3B;EAAA,KAAA,GAAQ,MAAA,SAAe,IAAA;EAAA;EAGvB,UAAA;AAAA;;AAMF;;UAAiB,kBAAA;EAEI;EAAnB,OAAA,GAAU,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EAAR;EAG/B,MAAA,EAAQ,wBAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-buddy",
3
- "version": "0.0.9-beta.0",
3
+ "version": "0.0.9",
4
4
  "description": "AI-powered cursor companion for web apps",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"file":"client-CevxN9EX.mjs","names":["clamp"],"sources":["../src/core/atoms.ts","../src/core/utils/error.ts","../src/core/services/audio-playback.ts","../src/core/utils/web-speech.ts","../src/core/services/browser-speech.ts","../src/core/services/live-transcription.ts","../src/core/bezier.ts","../src/core/services/pointer-controller.ts","../src/core/utils/annotations.ts","../src/core/utils/elements.ts","../src/core/utils/screenshot.ts","../src/core/services/screen-capture.ts","../src/core/services/tts-playback-queue.ts","../src/core/utils/audio.ts","../src/core/utils/audio-worklet.ts","../src/core/services/voice-capture.ts","../src/core/state-machine.ts","../src/core/utils/ui-stream-parser.ts","../src/core/utils/response-processor.ts","../src/core/client.ts"],"sourcesContent":["import { atom } from \"nanostores\"\nimport type { ConversationMessage, Point, PointingTarget } from \"./types\"\n\n/**\n * Nanostores atoms for reactive values that don't need state machine semantics.\n * These update frequently (e.g., 60fps audio levels) and are framework-agnostic.\n */\n\n// Audio level during recording (0-1, updates at ~60fps)\nexport const $audioLevel = atom<number>(0)\n\n// Mouse cursor position (real cursor, not buddy)\nexport const $cursorPosition = atom<Point>({ x: 0, y: 0 })\n\n// Buddy animated position (follows cursor with spring physics, or flies to target)\nexport const $buddyPosition = atom<Point>({ x: 0, y: 0 })\n\n// Buddy rotation in radians (direction of travel during pointing)\nexport const $buddyRotation = atom<number>(0)\n\n// Buddy scale (1.0 normal, up to 1.3 during flight)\nexport const $buddyScale = atom<number>(1)\n\n// Current pointing target parsed from AI response\nexport const $pointingTarget = atom<PointingTarget | null>(null)\n\n// Whether buddy overlay is enabled/visible\nexport const $isEnabled = atom<boolean>(true)\n\n// Whether TTS is currently playing\nexport const $isSpeaking = atom<boolean>(false)\n\n// Conversation history for context\nexport const $conversationHistory = atom<ConversationMessage[]>([])\n","/**\n * Normalize unknown thrown values into Error instances.\n */\nexport function toError(\n error: unknown,\n fallbackMessage: string = \"Unknown error\",\n): Error {\n if (error instanceof Error) {\n return error\n }\n\n if (typeof error === \"string\" && error) {\n return new Error(error)\n }\n\n return new Error(fallbackMessage)\n}\n","import type { AudioPlaybackPort } from \"../types\"\nimport { toError } from \"../utils/error\"\n\n/**\n * Framework-agnostic service for audio playback with abort support.\n */\nexport class AudioPlaybackService implements AudioPlaybackPort {\n private audio: HTMLAudioElement | null = null\n private currentUrl: string | null = null\n private settlePlayback:\n | ((outcome: \"resolve\" | \"reject\", error?: Error) => void)\n | null = null\n private removeAbortListener: (() => void) | null = null\n\n /**\n * Play audio from a blob. Stops any currently playing audio first.\n * @param blob - Audio blob to play\n * @param signal - Optional AbortSignal to cancel playback\n * @returns Promise that resolves when playback completes\n */\n async play(blob: Blob, signal?: AbortSignal): Promise<void> {\n // Stop any current playback\n this.stop()\n\n // Check if already aborted\n if (signal?.aborted) return\n\n const url = URL.createObjectURL(blob)\n this.currentUrl = url\n this.audio = new Audio(url)\n\n return new Promise<void>((resolve, reject) => {\n if (!this.audio) {\n this.cleanup()\n resolve()\n return\n }\n\n let settled = false\n const audio = this.audio\n\n const settle = (outcome: \"resolve\" | \"reject\", error?: Error) => {\n if (settled) return\n settled = true\n\n if (this.settlePlayback === settle) {\n this.settlePlayback = null\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n\n if (this.audio === audio) {\n this.audio.onended = null\n this.audio.onerror = null\n this.audio = null\n }\n\n this.cleanup()\n\n if (outcome === \"resolve\") {\n resolve()\n return\n }\n\n reject(error ?? new Error(\"Audio playback failed\"))\n }\n\n this.settlePlayback = settle\n\n const abortHandler = () => {\n audio.pause()\n settle(\"resolve\")\n }\n\n if (signal) {\n signal.addEventListener(\"abort\", abortHandler, { once: true })\n this.removeAbortListener = () => {\n signal.removeEventListener(\"abort\", abortHandler)\n }\n }\n\n this.audio.onended = () => {\n settle(\"resolve\")\n }\n\n this.audio.onerror = () => {\n settle(\"reject\", new Error(\"Audio playback failed\"))\n }\n\n this.audio.play().catch((err) => {\n settle(\"reject\", toError(err, \"Audio playback failed\"))\n })\n })\n }\n\n /**\n * Stop any currently playing audio.\n */\n stop(): void {\n if (this.audio) {\n this.audio.pause()\n }\n\n if (this.settlePlayback) {\n const settlePlayback = this.settlePlayback\n this.settlePlayback = null\n settlePlayback(\"resolve\")\n return\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n\n if (this.audio) {\n this.audio.onended = null\n this.audio.onerror = null\n this.audio = null\n }\n\n this.cleanup()\n }\n\n private cleanup(): void {\n if (this.currentUrl) {\n URL.revokeObjectURL(this.currentUrl)\n this.currentUrl = null\n }\n }\n}\n","/**\n * Normalize browser speech input and transcript output to a single-space form\n * so UI state and speech synthesis stay stable across browser event quirks.\n */\nexport function normalizeSpeechText(text: string): string {\n return text.replace(/\\s+/g, \" \").trim()\n}\n\n/**\n * Resolve the best browser locale to use for Web Speech APIs.\n *\n * We prefer the document language when the host app declares one, then fall\n * back to the browser locale, and finally to English as a stable default.\n */\nexport function resolveBrowserLanguage(): string {\n if (typeof document !== \"undefined\") {\n const documentLanguage = document.documentElement.lang.trim()\n if (documentLanguage) return documentLanguage\n }\n\n if (typeof navigator !== \"undefined\" && navigator.language) {\n return navigator.language\n }\n\n return \"en-US\"\n}\n","import type { BrowserSpeechPort } from \"../types\"\nimport { toError } from \"../utils/error\"\nimport {\n normalizeSpeechText,\n resolveBrowserLanguage,\n} from \"../utils/web-speech\"\n\nfunction getSpeechSynthesis(): SpeechSynthesis | undefined {\n return typeof globalThis.speechSynthesis === \"undefined\"\n ? undefined\n : globalThis.speechSynthesis\n}\n\nfunction getSpeechSynthesisUtterance():\n | typeof SpeechSynthesisUtterance\n | undefined {\n return typeof globalThis.SpeechSynthesisUtterance === \"undefined\"\n ? undefined\n : globalThis.SpeechSynthesisUtterance\n}\n\nfunction toSpeechError(event?: SpeechSynthesisErrorEvent): Error {\n const errorCode = event?.error\n\n return new Error(\n errorCode ? `Browser speech failed: ${errorCode}` : \"Browser speech failed\",\n )\n}\n\n/**\n * Browser-backed speech synthesis using the Web Speech API.\n */\nexport class BrowserSpeechService implements BrowserSpeechPort {\n private removeAbortListener: (() => void) | null = null\n private settleSpeech:\n | ((outcome: \"resolve\" | \"reject\", error?: Error) => void)\n | null = null\n private utterance: SpeechSynthesisUtterance | null = null\n\n /**\n * Report whether this runtime exposes the browser Web Speech synthesis APIs.\n */\n isAvailable(): boolean {\n return Boolean(getSpeechSynthesis() && getSpeechSynthesisUtterance())\n }\n\n /**\n * Speak a single text segment in the browser.\n *\n * Each queue item owns its own utterance. We only stop an existing utterance\n * when this service still has one in flight, so streamed playback does not\n * spam global `speechSynthesis.cancel()` between already-completed segments.\n */\n async speak(text: string, signal?: AbortSignal): Promise<void> {\n const speechSynthesis = getSpeechSynthesis()\n const SpeechSynthesisUtteranceCtor = getSpeechSynthesisUtterance()\n\n if (!speechSynthesis || !SpeechSynthesisUtteranceCtor) {\n throw new Error(\"Browser speech is not supported\")\n }\n\n if (this.hasActiveSpeech()) {\n this.stop()\n }\n\n const normalizedText = normalizeSpeechText(text)\n if (!normalizedText || signal?.aborted) return\n\n const utterance = new SpeechSynthesisUtteranceCtor(normalizedText)\n utterance.lang = resolveBrowserLanguage()\n this.utterance = utterance\n\n return new Promise<void>((resolve, reject) => {\n let settled = false\n\n const settle = (outcome: \"resolve\" | \"reject\", error?: Error) => {\n if (settled) return\n settled = true\n\n if (this.settleSpeech === settle) {\n this.settleSpeech = null\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n this.clearUtterance(utterance)\n\n if (outcome === \"resolve\") {\n resolve()\n return\n }\n\n reject(error ?? new Error(\"Browser speech failed\"))\n }\n\n this.settleSpeech = settle\n\n const abortHandler = () => {\n try {\n speechSynthesis.cancel()\n } catch {\n // Ignore cancel failures during abort cleanup.\n }\n\n settle(\"resolve\")\n }\n\n if (signal) {\n signal.addEventListener(\"abort\", abortHandler, { once: true })\n this.removeAbortListener = () => {\n signal.removeEventListener(\"abort\", abortHandler)\n }\n }\n\n utterance.onend = () => {\n settle(\"resolve\")\n }\n\n utterance.onerror = (event) => {\n if (signal?.aborted) {\n settle(\"resolve\")\n return\n }\n\n settle(\"reject\", toSpeechError(event))\n }\n\n try {\n speechSynthesis.speak(utterance)\n } catch (error) {\n settle(\"reject\", toError(error, \"Browser speech failed to start\"))\n }\n })\n }\n\n /**\n * Stop the current utterance owned by this service, if one is active.\n *\n * We intentionally do nothing when the service is idle so we do not cancel\n * unrelated speech synthesis work that host apps may be doing elsewhere.\n */\n stop(): void {\n if (!this.hasActiveSpeech()) {\n return\n }\n\n const speechSynthesis = getSpeechSynthesis()\n\n if (speechSynthesis) {\n try {\n speechSynthesis.cancel()\n } catch {\n // Ignore cancel failures during cleanup.\n }\n }\n\n if (this.settleSpeech) {\n const settleSpeech = this.settleSpeech\n this.settleSpeech = null\n settleSpeech(\"resolve\")\n return\n }\n\n this.removeAbortListener?.()\n this.removeAbortListener = null\n this.clearUtterance(this.utterance)\n }\n\n private hasActiveSpeech(): boolean {\n return Boolean(this.utterance || this.settleSpeech)\n }\n\n private clearUtterance(utterance: SpeechSynthesisUtterance | null): void {\n if (!utterance) return\n\n utterance.onend = null\n utterance.onerror = null\n\n if (this.utterance === utterance) {\n this.utterance = null\n }\n }\n}\n","import type { LiveTranscriptionPort } from \"../types\"\nimport { toError } from \"../utils/error\"\nimport {\n normalizeSpeechText,\n resolveBrowserLanguage,\n} from \"../utils/web-speech\"\n\ninterface SpeechRecognitionAlternativeLike {\n transcript: string\n}\n\ninterface SpeechRecognitionResultLike {\n isFinal: boolean\n length: number\n [index: number]: SpeechRecognitionAlternativeLike\n}\n\ninterface SpeechRecognitionResultListLike {\n length: number\n [index: number]: SpeechRecognitionResultLike\n}\n\ninterface SpeechRecognitionEventLike {\n results: SpeechRecognitionResultListLike\n}\n\ninterface SpeechRecognitionErrorEventLike {\n error?: string\n message?: string\n}\n\ninterface SpeechRecognitionLike {\n continuous: boolean\n interimResults: boolean\n lang: string\n maxAlternatives: number\n onend: (() => void) | null\n onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null\n onresult: ((event: SpeechRecognitionEventLike) => void) | null\n onstart: (() => void) | null\n abort(): void\n start(): void\n stop(): void\n}\n\ntype SpeechRecognitionConstructor = new () => SpeechRecognitionLike\n\ntype GlobalWithSpeechRecognition = typeof globalThis & {\n SpeechRecognition?: SpeechRecognitionConstructor\n webkitSpeechRecognition?: SpeechRecognitionConstructor\n}\n\nfunction getSpeechRecognitionConstructor():\n | SpeechRecognitionConstructor\n | undefined {\n const globalScope = globalThis as GlobalWithSpeechRecognition\n\n return globalScope.SpeechRecognition ?? globalScope.webkitSpeechRecognition\n}\n\nfunction toRecognitionError(event?: SpeechRecognitionErrorEventLike): Error {\n const errorCode = event?.error\n const message =\n event?.message ||\n (errorCode\n ? `Browser transcription failed: ${errorCode}`\n : \"Browser transcription failed\")\n\n return new Error(message)\n}\n\nfunction buildTranscripts(results: SpeechRecognitionResultListLike): {\n finalTranscript: string\n liveTranscript: string\n} {\n let finalTranscript = \"\"\n let interimTranscript = \"\"\n\n // Web Speech returns the running recognition result list on every event.\n // We rebuild both views each time so the client always sees the freshest\n // \"confirmed + in-progress\" transcript.\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index]\n const alternative = result?.[0]\n const transcript = alternative?.transcript ?? \"\"\n\n if (!transcript) continue\n\n if (result.isFinal) {\n finalTranscript += `${transcript} `\n } else {\n interimTranscript += `${transcript} `\n }\n }\n\n const normalizedFinal = normalizeSpeechText(finalTranscript)\n const normalizedInterim = normalizeSpeechText(interimTranscript)\n\n return {\n finalTranscript: normalizedFinal,\n liveTranscript: normalizeSpeechText(\n [normalizedFinal, normalizedInterim].filter(Boolean).join(\" \"),\n ),\n }\n}\n\n/**\n * Browser-backed live transcription using the Web Speech API.\n */\nexport class LiveTranscriptionService implements LiveTranscriptionPort {\n private finalTranscript = \"\"\n private hasStarted = false\n private hasEnded = false\n private lastError: Error | null = null\n private partialCallback: ((text: string) => void) | null = null\n private recognition: SpeechRecognitionLike | null = null\n private startReject: ((reason?: unknown) => void) | null = null\n private startResolve: (() => void) | null = null\n private stopReject: ((reason?: unknown) => void) | null = null\n private stopResolve: ((value: string) => void) | null = null\n\n isAvailable(): boolean {\n return Boolean(getSpeechRecognitionConstructor())\n }\n\n /**\n * Register a callback for the latest browser transcript while the user is\n * still speaking.\n */\n onPartial(callback: (text: string) => void): void {\n this.partialCallback = callback\n }\n\n /**\n * Start a new Web Speech recognition session.\n */\n async start(): Promise<void> {\n const SpeechRecognitionCtor = getSpeechRecognitionConstructor()\n if (!SpeechRecognitionCtor) {\n throw new Error(\"Browser transcription is not supported\")\n }\n\n // Each push-to-talk turn owns a fresh recognition session. We clear any\n // previous session first so late events do not leak into the next turn.\n this.dispose()\n\n const recognition = new SpeechRecognitionCtor()\n this.recognition = recognition\n recognition.continuous = true\n recognition.interimResults = true\n recognition.maxAlternatives = 1\n recognition.lang = resolveBrowserLanguage()\n recognition.onstart = () => {\n this.hasStarted = true\n this.startResolve?.()\n this.startResolve = null\n this.startReject = null\n }\n recognition.onresult = (event) => {\n const transcripts = buildTranscripts(event.results)\n this.finalTranscript = transcripts.finalTranscript\n this.partialCallback?.(transcripts.liveTranscript)\n }\n recognition.onerror = (event) => {\n this.lastError = toRecognitionError(event)\n\n // Errors before `onstart` should reject startup immediately. Errors after\n // startup are handled when the session ends or when stop() awaits it.\n if (!this.hasStarted) {\n this.startReject?.(this.lastError)\n this.startResolve = null\n this.startReject = null\n }\n }\n recognition.onend = () => {\n this.hasEnded = true\n\n // Some browsers can jump straight to `end` when recognition is blocked\n // or cancelled before startup. Convert that into a startup failure.\n if (!this.hasStarted) {\n const error =\n this.lastError ??\n new Error(\"Browser transcription ended before it could start\")\n\n this.startReject?.(error)\n this.startResolve = null\n this.startReject = null\n }\n\n // Once stop() is waiting, settle it on the terminal `end` event so we\n // capture the last finalized transcript from the browser.\n if (this.stopResolve || this.stopReject) {\n if (this.lastError) {\n this.stopReject?.(this.lastError)\n } else {\n this.stopResolve?.(normalizeSpeechText(this.finalTranscript))\n }\n\n this.stopResolve = null\n this.stopReject = null\n }\n }\n\n const started = new Promise<void>((resolve, reject) => {\n this.startResolve = resolve\n this.startReject = reject\n })\n\n try {\n recognition.start()\n } catch (error) {\n this.clearRecognition()\n throw toError(error, \"Browser transcription failed to start\")\n }\n\n try {\n await started\n } catch (error) {\n this.clearRecognition()\n throw toError(error, \"Browser transcription failed to start\")\n }\n }\n\n /**\n * Stop the current recognition session and resolve with the final transcript.\n */\n async stop(): Promise<string> {\n if (!this.recognition) {\n if (this.lastError) {\n throw this.lastError\n }\n\n return normalizeSpeechText(this.finalTranscript)\n }\n\n if (this.hasEnded) {\n const transcript = normalizeSpeechText(this.finalTranscript)\n const error = this.lastError\n this.clearRecognition()\n\n if (error) {\n throw error\n }\n\n return transcript\n }\n\n const recognition = this.recognition\n\n const transcript = await new Promise<string>((resolve, reject) => {\n this.stopResolve = resolve\n this.stopReject = reject\n\n try {\n recognition.stop()\n } catch (error) {\n reject(toError(error, \"Browser transcription failed to stop\"))\n }\n }).finally(() => {\n this.clearRecognition()\n })\n\n return normalizeSpeechText(transcript)\n }\n\n /**\n * Abort the current recognition session and reset the service for reuse.\n */\n dispose(): void {\n if (this.recognition) {\n try {\n this.recognition.abort()\n } catch {\n // Ignore abort failures during cleanup.\n }\n }\n\n this.startReject?.(new Error(\"Browser transcription aborted\"))\n this.stopResolve?.(normalizeSpeechText(this.finalTranscript))\n this.startResolve = null\n this.startReject = null\n this.stopResolve = null\n this.stopReject = null\n this.clearRecognition()\n this.resetSessionState()\n }\n\n private clearRecognition(): void {\n if (!this.recognition) return\n\n // Drop event handlers explicitly so a late browser callback cannot mutate\n // the service after the turn has already moved on.\n this.recognition.onstart = null\n this.recognition.onresult = null\n this.recognition.onerror = null\n this.recognition.onend = null\n this.recognition = null\n }\n\n private resetSessionState(): void {\n this.finalTranscript = \"\"\n this.hasStarted = false\n this.hasEnded = false\n this.lastError = null\n // Clear the live transcript view at the start and end of each turn.\n this.partialCallback?.(\"\")\n }\n}\n","import type { Point } from \"./types\"\n\n/**\n * Bezier flight animation for cursor pointing.\n */\n\n/**\n * Quadratic bezier curve: B(t) = (1-t)²P₀ + 2(1-t)t·P₁ + t²P₂\n */\nfunction quadraticBezier(p0: Point, p1: Point, p2: Point, t: number): Point {\n const oneMinusT = 1 - t\n return {\n x: oneMinusT * oneMinusT * p0.x + 2 * oneMinusT * t * p1.x + t * t * p2.x,\n y: oneMinusT * oneMinusT * p0.y + 2 * oneMinusT * t * p1.y + t * t * p2.y,\n }\n}\n\n/**\n * Bezier tangent (derivative): B'(t) = 2(1-t)(P₁-P₀) + 2t(P₂-P₁)\n */\nfunction bezierTangent(p0: Point, p1: Point, p2: Point, t: number): Point {\n const oneMinusT = 1 - t\n return {\n x: 2 * oneMinusT * (p1.x - p0.x) + 2 * t * (p2.x - p1.x),\n y: 2 * oneMinusT * (p1.y - p0.y) + 2 * t * (p2.y - p1.y),\n }\n}\n\n/**\n * Ease-in-out cubic for smooth acceleration/deceleration\n */\nfunction easeInOutCubic(t: number): number {\n return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2\n}\n\nexport interface BezierFlightCallbacks {\n onFrame: (position: Point, rotation: number, scale: number) => void\n onComplete: () => void\n}\n\n/**\n * Animate cursor along a parabolic bezier arc from start to end.\n * Used when the AI points at a UI element.\n *\n * @param from - Starting position\n * @param to - Target position\n * @param durationMs - Flight duration in milliseconds\n * @param callbacks - Frame and completion callbacks\n * @returns Cancel function to stop the animation\n */\nexport function animateBezierFlight(\n from: Point,\n to: Point,\n durationMs: number,\n callbacks: BezierFlightCallbacks,\n): () => void {\n const startTime = performance.now()\n const distance = Math.hypot(to.x - from.x, to.y - from.y)\n\n // Control point: offset upward by 20% of distance (creates parabolic arc)\n const controlPoint: Point = {\n x: (from.x + to.x) / 2,\n y: Math.min(from.y, to.y) - distance * 0.2,\n }\n\n let animationFrameId: number\n\n function animate(now: number) {\n const elapsed = now - startTime\n const linearProgress = Math.min(elapsed / durationMs, 1)\n const easedProgress = easeInOutCubic(linearProgress)\n\n const position = quadraticBezier(from, controlPoint, to, easedProgress)\n const tangent = bezierTangent(from, controlPoint, to, easedProgress)\n const rotation = Math.atan2(tangent.y, tangent.x)\n\n // Scale pulse: grows to 1.3x at midpoint, returns to 1x\n const scale = 1 + Math.sin(linearProgress * Math.PI) * 0.3\n\n callbacks.onFrame(position, rotation, scale)\n\n if (linearProgress < 1) {\n animationFrameId = requestAnimationFrame(animate)\n } else {\n callbacks.onComplete()\n }\n }\n\n animationFrameId = requestAnimationFrame(animate)\n\n return () => cancelAnimationFrame(animationFrameId)\n}\n","import {\n $buddyPosition,\n $buddyRotation,\n $buddyScale,\n $cursorPosition,\n $pointingTarget,\n} from \"../atoms\"\nimport { animateBezierFlight } from \"../bezier\"\nimport type { PointerControllerPort, PointingTarget } from \"../types\"\n\nconst POINTING_LOCK_TIMEOUT_MS = 10_000\n\ntype PointerMode = \"follow\" | \"flying\" | \"anchored\"\n\n/**\n * Controller for cursor pointing behavior.\n * Manages the pointer state machine (follow -> flying -> anchored -> follow)\n * and cursor animation.\n */\nexport class PointerController implements PointerControllerPort {\n private mode: PointerMode = \"follow\"\n private cancelAnimation: (() => void) | null = null\n private releaseTimeout: ReturnType<typeof setTimeout> | null = null\n private listeners = new Set<() => void>()\n\n /**\n * Animate cursor to point at a target.\n */\n pointAt(target: PointingTarget): void {\n // Clear any previous pointing state\n this.release()\n\n this.mode = \"flying\"\n $pointingTarget.set(target)\n\n const startPos = $buddyPosition.get()\n const endPos = { x: target.x, y: target.y }\n\n this.cancelAnimation = animateBezierFlight(startPos, endPos, 800, {\n onFrame: (position, rotation, scale) => {\n $buddyPosition.set(position)\n $buddyRotation.set(rotation)\n $buddyScale.set(scale)\n },\n onComplete: () => {\n this.cancelAnimation = null\n this.mode = \"anchored\"\n $buddyPosition.set(endPos)\n $buddyRotation.set(0)\n $buddyScale.set(1)\n this.scheduleRelease()\n this.notify()\n },\n })\n\n this.notify()\n }\n\n /**\n * Release the cursor from pointing mode back to follow mode.\n */\n release(): void {\n // Cancel any in-progress animation\n if (this.cancelAnimation) {\n this.cancelAnimation()\n this.cancelAnimation = null\n }\n\n // Clear release timeout\n if (this.releaseTimeout) {\n clearTimeout(this.releaseTimeout)\n this.releaseTimeout = null\n }\n\n // Reset to follow mode\n this.mode = \"follow\"\n $pointingTarget.set(null)\n $buddyPosition.set($cursorPosition.get())\n $buddyRotation.set(0)\n $buddyScale.set(1)\n\n this.notify()\n }\n\n /**\n * Check if cursor is currently pointing (flying or anchored).\n */\n isPointing(): boolean {\n return this.mode !== \"follow\"\n }\n\n /**\n * Get current pointer mode.\n */\n getMode(): PointerMode {\n return this.mode\n }\n\n /**\n * Subscribe to pointer state changes.\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /**\n * Update buddy position to follow cursor when in follow mode.\n * Call this on cursor position changes.\n */\n updateFollowPosition(): void {\n if (this.mode === \"follow\") {\n $buddyPosition.set($cursorPosition.get())\n $buddyRotation.set(0)\n $buddyScale.set(1)\n }\n }\n\n private scheduleRelease(): void {\n this.releaseTimeout = setTimeout(() => {\n this.releaseTimeout = null\n this.release()\n }, POINTING_LOCK_TIMEOUT_MS)\n }\n\n private notify(): void {\n this.listeners.forEach((listener) => listener())\n }\n}\n","/**\n * Annotation drawing for screenshots.\n * Draws numbered markers on interactive elements.\n */\n\nimport type { MarkerMap } from \"./elements\"\n\n/**\n * Annotation style configuration.\n */\ninterface AnnotationStyle {\n /** Border color for marker boxes */\n borderColor: string\n /** Background color for number labels */\n labelBackground: string\n /** Text color for number labels */\n labelColor: string\n /** Border width in pixels */\n borderWidth: number\n /** Font size for labels */\n fontSize: number\n /** Padding around label text */\n labelPadding: number\n}\n\nconst DEFAULT_STYLE: AnnotationStyle = {\n borderColor: \"rgba(255, 0, 0, 0.8)\",\n labelBackground: \"rgba(255, 0, 0, 0.9)\",\n labelColor: \"#ffffff\",\n borderWidth: 2,\n fontSize: 11,\n labelPadding: 4,\n}\n\n/**\n * Draw annotation markers onto a canvas.\n * Modifies the canvas in place.\n *\n * @param ctx Canvas 2D context to draw on\n * @param markers Marker map from element discovery\n * @param style Optional style overrides\n */\nexport function drawAnnotations(\n ctx: CanvasRenderingContext2D,\n markers: MarkerMap,\n style: Partial<AnnotationStyle> = {},\n): void {\n const s = { ...DEFAULT_STYLE, ...style }\n\n ctx.save()\n\n for (const marker of markers.values()) {\n const { rect, id } = marker\n\n // Draw border box\n ctx.strokeStyle = s.borderColor\n ctx.lineWidth = s.borderWidth\n ctx.strokeRect(rect.left, rect.top, rect.width, rect.height)\n\n // Draw label\n const label = String(id)\n ctx.font = `bold ${s.fontSize}px monospace`\n const textMetrics = ctx.measureText(label)\n const textWidth = textMetrics.width\n const textHeight = s.fontSize\n\n // Label position: top-left of element, or inside if near viewport top\n const labelWidth = textWidth + s.labelPadding * 2\n const labelHeight = textHeight + s.labelPadding\n const labelX = rect.left - s.borderWidth\n const labelY = rect.top < labelHeight + 4 ? rect.top + 2 : rect.top - labelHeight\n\n // Label background\n ctx.fillStyle = s.labelBackground\n ctx.beginPath()\n ctx.roundRect(labelX, labelY, labelWidth, labelHeight, 2)\n ctx.fill()\n\n // Label text\n ctx.fillStyle = s.labelColor\n ctx.textBaseline = \"top\"\n ctx.fillText(label, labelX + s.labelPadding, labelY + s.labelPadding / 2)\n }\n\n ctx.restore()\n}\n\n/**\n * Create an annotated copy of a canvas.\n * Does not modify the original canvas.\n *\n * @param sourceCanvas Original screenshot canvas\n * @param markers Marker map from element discovery\n * @returns New canvas with annotations drawn\n */\nexport function createAnnotatedCanvas(\n sourceCanvas: HTMLCanvasElement,\n markers: MarkerMap,\n): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\")\n canvas.width = sourceCanvas.width\n canvas.height = sourceCanvas.height\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2D context\")\n }\n\n // Copy source image\n ctx.drawImage(sourceCanvas, 0, 0)\n\n // Draw annotations\n drawAnnotations(ctx, markers)\n\n return canvas\n}\n\n/**\n * Generate marker context string for AI prompt.\n * Lists available markers with their descriptions.\n *\n * @param markers Marker map from element discovery\n * @returns Formatted string listing markers\n */\nexport function generateMarkerContext(markers: MarkerMap): string {\n if (markers.size === 0) {\n return \"No interactive elements detected.\"\n }\n\n const lines = [\"Interactive elements (use marker number to point):\"]\n\n for (const marker of markers.values()) {\n lines.push(` ${marker.id}: ${marker.description}`)\n }\n\n return lines.join(\"\\n\")\n}\n","/**\n * Element discovery for annotated screenshots.\n * Finds visible interactive elements and assigns marker IDs.\n */\n\n/** Max characters for element descriptions passed to the model. */\nconst MAX_DESCRIPTION_LENGTH = 50\n\n/** Pixels tolerance for grouping elements into the same visual row. */\nconst ROW_TOLERANCE_PX = 20\n\n/**\n * Interactive element selectors - elements users would want to click/interact with.\n * Mirrors accessibility roles from agent-browser but using CSS selectors.\n */\nconst INTERACTIVE_SELECTORS = [\n // Buttons\n \"button\",\n '[role=\"button\"]',\n 'input[type=\"button\"]',\n 'input[type=\"submit\"]',\n 'input[type=\"reset\"]',\n\n // Links\n \"a[href]\",\n '[role=\"link\"]',\n\n // Form inputs\n 'input:not([type=\"hidden\"])',\n \"textarea\",\n \"select\",\n '[role=\"textbox\"]',\n '[role=\"searchbox\"]',\n '[role=\"combobox\"]',\n '[role=\"listbox\"]',\n '[role=\"slider\"]',\n '[role=\"spinbutton\"]',\n\n // Checkboxes and radios\n '[role=\"checkbox\"]',\n '[role=\"radio\"]',\n '[role=\"switch\"]',\n\n // Menu items\n '[role=\"menuitem\"]',\n '[role=\"menuitemcheckbox\"]',\n '[role=\"menuitemradio\"]',\n '[role=\"option\"]',\n\n // Tabs\n '[role=\"tab\"]',\n '[role=\"treeitem\"]',\n\n // Media controls\n \"video\",\n \"audio\",\n\n // Custom interactive elements (opt-in)\n \"[data-cursor-buddy-interactive]\",\n]\n\n/**\n * Element marker with reference to actual DOM element.\n */\nexport interface ElementMarker {\n /** Sequential marker ID (1, 2, 3...) */\n id: number\n /** Reference to the actual DOM element */\n element: Element\n /** Bounding rect at time of capture */\n rect: DOMRect\n /** Brief description for AI context */\n description: string\n}\n\n/**\n * Map of marker ID to element marker.\n */\nexport type MarkerMap = Map<number, ElementMarker>\n\n/**\n * Check if an element is visible in the viewport.\n */\nfunction isElementVisible(\n element: Element,\n rect: DOMRect = element.getBoundingClientRect(),\n): boolean {\n // Has size\n if (rect.width <= 0 || rect.height <= 0) return false\n\n // In viewport\n if (\n rect.bottom < 0 ||\n rect.top > window.innerHeight ||\n rect.right < 0 ||\n rect.left > window.innerWidth\n ) {\n return false\n }\n\n // Not hidden via CSS\n const style = window.getComputedStyle(element)\n if (style.visibility === \"hidden\" || style.display === \"none\") return false\n if (Number.parseFloat(style.opacity) === 0) return false\n\n return true\n}\n\nfunction truncateDescription(value: string): string {\n return value.slice(0, MAX_DESCRIPTION_LENGTH)\n}\n\n/**\n * Generate a brief description for an element.\n */\nfunction describeElement(element: Element): string {\n const tag = element.tagName.toLowerCase()\n\n // Try aria-label first\n const ariaLabel = element.getAttribute(\"aria-label\")\n if (ariaLabel) return truncateDescription(ariaLabel)\n\n // Try text content for buttons/links\n if (tag === \"button\" || tag === \"a\") {\n const text = element.textContent?.trim()\n if (text) return truncateDescription(text)\n }\n\n // Try placeholder for inputs\n if (tag === \"input\" || tag === \"textarea\") {\n const placeholder = element.getAttribute(\"placeholder\")\n if (placeholder) return truncateDescription(placeholder)\n\n const type = element.getAttribute(\"type\") || \"text\"\n return `${type} input`\n }\n\n // Try alt for images\n if (tag === \"img\") {\n const alt = element.getAttribute(\"alt\")\n if (alt) return truncateDescription(alt)\n return \"image\"\n }\n\n // Try role\n const role = element.getAttribute(\"role\")\n if (role) return role\n\n // Fallback to tag name\n return tag\n}\n\ninterface VisibleInteractiveElement {\n element: Element\n rect: DOMRect\n}\n\nfunction collectVisibleInteractiveElements(): VisibleInteractiveElement[] {\n const selector = INTERACTIVE_SELECTORS.join(\",\")\n const allElements = document.querySelectorAll(selector)\n const visible: VisibleInteractiveElement[] = []\n\n for (const element of allElements) {\n const rect = element.getBoundingClientRect()\n if (!isElementVisible(element, rect)) continue\n\n visible.push({ element, rect })\n }\n\n visible.sort((a, b) => {\n // Primary: top position, grouped into approximate rows\n const rowDiff =\n Math.floor(a.rect.top / ROW_TOLERANCE_PX) -\n Math.floor(b.rect.top / ROW_TOLERANCE_PX)\n if (rowDiff !== 0) return rowDiff\n\n // Secondary: left position\n return a.rect.left - b.rect.left\n })\n\n return visible\n}\n\n/**\n * Find all visible interactive elements in the viewport.\n * Returns elements sorted by visual position (top-left to bottom-right).\n */\nexport function findInteractiveElements(): Element[] {\n return collectVisibleInteractiveElements().map(({ element }) => element)\n}\n\n/**\n * Create marker map from visible interactive elements.\n * Assigns sequential IDs starting from 1.\n */\nexport function createMarkerMap(): MarkerMap {\n const elements = collectVisibleInteractiveElements()\n const map: MarkerMap = new Map()\n\n elements.forEach(({ element, rect }, index) => {\n const id = index + 1\n map.set(id, {\n id,\n element,\n rect,\n description: describeElement(element),\n })\n })\n\n return map\n}\n\n/**\n * Get the center point of an element in viewport coordinates.\n */\nexport function getElementCenter(element: Element): { x: number; y: number } {\n const rect = element.getBoundingClientRect()\n return {\n x: Math.round(rect.left + rect.width / 2),\n y: Math.round(rect.top + rect.height / 2),\n }\n}\n\n/**\n * Resolve a marker ID to viewport coordinates.\n * Returns null if marker not found or element no longer visible.\n */\nexport function resolveMarkerToCoordinates(\n markerMap: MarkerMap,\n markerId: number,\n): { x: number; y: number } | null {\n const marker = markerMap.get(markerId)\n if (!marker) return null\n\n // Check element still exists and is visible\n if (!document.contains(marker.element)) return null\n if (!isElementVisible(marker.element)) return null\n\n return getElementCenter(marker.element)\n}\n","import html2canvas from \"html2canvas-pro\"\nimport type { AnnotatedScreenshotResult, ScreenshotResult } from \"../types\"\nimport { createAnnotatedCanvas, generateMarkerContext } from \"./annotations\"\nimport { createMarkerMap } from \"./elements\"\n\nconst CLONE_RESOURCE_TIMEOUT_MS = 3000\n\n/** Maximum width for compressed screenshots (maintains aspect ratio) */\nconst MAX_SCREENSHOT_WIDTH = 1280\n\n/** JPEG quality for compressed screenshots (0-1) */\nconst JPEG_QUALITY = 0.8\n\n/**\n * Compression result with compressed image data and dimensions.\n */\ninterface CompressionResult {\n /** Base64-encoded compressed image data */\n imageData: string\n /** Width of the compressed image */\n width: number\n /** Height of the compressed image */\n height: number\n}\n\n/**\n * Compress a canvas image by downscaling and converting to JPEG.\n * Maintains aspect ratio and falls back to original if compression fails.\n *\n * @param sourceCanvas - The source canvas to compress\n * @param maxWidth - Maximum width for the compressed image (default: MAX_SCREENSHOT_WIDTH)\n * @param quality - JPEG quality 0-1 (default: JPEG_QUALITY)\n * @returns Compression result with compressed image data and dimensions\n */\nfunction compressImage(\n sourceCanvas: HTMLCanvasElement,\n maxWidth: number = MAX_SCREENSHOT_WIDTH,\n quality: number = JPEG_QUALITY,\n): CompressionResult {\n const sourceWidth = sourceCanvas.width\n const sourceHeight = sourceCanvas.height\n\n // If source is already smaller than max width, just convert to JPEG\n if (sourceWidth <= maxWidth) {\n return {\n imageData: sourceCanvas.toDataURL(\"image/jpeg\", quality),\n width: sourceWidth,\n height: sourceHeight,\n }\n }\n\n // Calculate scaled dimensions maintaining aspect ratio\n const scale = maxWidth / sourceWidth\n const targetWidth = Math.round(maxWidth)\n const targetHeight = Math.round(sourceHeight * scale)\n\n // Create canvas for compressed image\n const canvas = document.createElement(\"canvas\")\n canvas.width = targetWidth\n canvas.height = targetHeight\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) {\n // Fallback: return original as JPEG\n return {\n imageData: sourceCanvas.toDataURL(\"image/jpeg\", quality),\n width: sourceWidth,\n height: sourceHeight,\n }\n }\n\n // Use better quality scaling\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = \"high\"\n\n // Draw scaled image\n ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight)\n\n // Export as JPEG\n return {\n imageData: canvas.toDataURL(\"image/jpeg\", quality),\n width: targetWidth,\n height: targetHeight,\n }\n}\n\nfunction getCaptureMetrics() {\n return {\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n }\n}\n\nfunction waitForNextPaint(doc: Document): Promise<void> {\n const view = doc.defaultView\n if (!view?.requestAnimationFrame) return Promise.resolve()\n\n return new Promise((resolve) => {\n view.requestAnimationFrame(() => {\n view.requestAnimationFrame(() => resolve())\n })\n })\n}\n\nfunction isStylesheetReady(link: HTMLLinkElement): boolean {\n const sheet = link.sheet\n if (!sheet) return false\n\n try {\n void sheet.cssRules\n return true\n } catch (error) {\n return error instanceof DOMException && error.name === \"SecurityError\"\n }\n}\n\nfunction waitForStylesheetLink(link: HTMLLinkElement): Promise<void> {\n if (isStylesheetReady(link)) return Promise.resolve()\n\n return new Promise((resolve) => {\n let settled = false\n let timeoutId = 0\n\n const finish = () => {\n if (settled) return\n settled = true\n window.clearTimeout(timeoutId)\n link.removeEventListener(\"load\", handleReady)\n link.removeEventListener(\"error\", handleReady)\n resolve()\n }\n\n const handleReady = () => {\n if (isStylesheetReady(link)) {\n finish()\n return\n }\n\n window.requestAnimationFrame(() => {\n if (isStylesheetReady(link)) {\n finish()\n }\n })\n }\n\n timeoutId = window.setTimeout(finish, CLONE_RESOURCE_TIMEOUT_MS)\n link.addEventListener(\"load\", handleReady, { once: true })\n link.addEventListener(\"error\", finish, { once: true })\n\n handleReady()\n })\n}\n\nasync function waitForClonedDocumentStyles(doc: Document): Promise<void> {\n const stylesheetLinks = Array.from(\n doc.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"][href]'),\n )\n\n await Promise.all(stylesheetLinks.map(waitForStylesheetLink))\n\n if (doc.fonts?.ready) {\n await doc.fonts.ready\n }\n\n await waitForNextPaint(doc)\n}\n\nfunction getHtml2CanvasOptions(\n captureMetrics: ReturnType<typeof getCaptureMetrics>,\n) {\n return {\n scale: 1,\n useCORS: true,\n logging: false,\n width: captureMetrics.viewportWidth,\n height: captureMetrics.viewportHeight,\n windowWidth: captureMetrics.viewportWidth,\n windowHeight: captureMetrics.viewportHeight,\n x: window.scrollX,\n y: window.scrollY,\n scrollX: window.scrollX,\n scrollY: window.scrollY,\n // In production Next.js emits external stylesheet links; wait for the\n // cloned iframe to finish applying them before html2canvas renders.\n onclone: async (doc: Document) => {\n await waitForClonedDocumentStyles(doc)\n },\n } as const\n}\n\n/**\n * Create a fallback canvas when screenshot capture fails.\n * Returns a simple gray canvas with an error message.\n */\nfunction createFallbackCanvas(): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\")\n canvas.width = window.innerWidth\n canvas.height = window.innerHeight\n\n const ctx = canvas.getContext(\"2d\")\n if (ctx) {\n ctx.fillStyle = \"#f0f0f0\"\n ctx.fillRect(0, 0, canvas.width, canvas.height)\n ctx.fillStyle = \"#666\"\n ctx.font = \"16px sans-serif\"\n ctx.textAlign = \"center\"\n ctx.fillText(\"Screenshot unavailable\", canvas.width / 2, canvas.height / 2)\n }\n\n return canvas\n}\n\n/**\n * Capture a screenshot of the current viewport.\n * Uses html2canvas to render the DOM to a canvas, then compresses to JPEG.\n * Falls back to a placeholder if capture fails (e.g., due to unsupported CSS).\n */\nexport async function captureViewport(): Promise<ScreenshotResult> {\n const captureMetrics = getCaptureMetrics()\n let canvas: HTMLCanvasElement\n\n try {\n canvas = await html2canvas(\n document.body,\n getHtml2CanvasOptions(captureMetrics),\n )\n } catch {\n canvas = createFallbackCanvas()\n }\n\n // Compress the screenshot (with fallback to uncompressed on error)\n let compressed: CompressionResult\n try {\n compressed = compressImage(canvas)\n } catch {\n // Fallback: use uncompressed PNG\n compressed = {\n imageData: canvas.toDataURL(\"image/png\"),\n width: canvas.width,\n height: canvas.height,\n }\n }\n\n return {\n imageData: compressed.imageData,\n width: compressed.width,\n height: compressed.height,\n viewportWidth: captureMetrics.viewportWidth,\n viewportHeight: captureMetrics.viewportHeight,\n }\n}\n\n/**\n * Capture an annotated screenshot of the current viewport.\n * Interactive elements are marked with numbered labels.\n * Returns both the annotated image and a marker map for resolving IDs.\n */\nexport async function captureAnnotatedViewport(): Promise<AnnotatedScreenshotResult> {\n const captureMetrics = getCaptureMetrics()\n\n // 1. Discover interactive elements BEFORE capturing screenshot\n // (so rects are accurate to what's visible)\n const markerMap = createMarkerMap()\n\n // 2. Capture screenshot\n let sourceCanvas: HTMLCanvasElement\n try {\n sourceCanvas = await html2canvas(\n document.body,\n getHtml2CanvasOptions(captureMetrics),\n )\n } catch {\n sourceCanvas = createFallbackCanvas()\n }\n\n // 3. Create a fresh canvas and draw annotations on it\n // (html2canvas leaves dirty context state - transforms, clipping, etc.)\n const canvas =\n markerMap.size > 0\n ? createAnnotatedCanvas(sourceCanvas, markerMap)\n : sourceCanvas\n\n // 4. Generate marker context for AI\n const markerContext = generateMarkerContext(markerMap)\n\n // 5. Compress the screenshot (with fallback to uncompressed on error)\n let compressed: CompressionResult\n try {\n compressed = compressImage(canvas)\n } catch {\n // Fallback: use uncompressed PNG\n compressed = {\n imageData: canvas.toDataURL(\"image/png\"),\n width: canvas.width,\n height: canvas.height,\n }\n }\n\n return {\n imageData: compressed.imageData,\n width: compressed.width,\n height: compressed.height,\n viewportWidth: captureMetrics.viewportWidth,\n viewportHeight: captureMetrics.viewportHeight,\n markerMap,\n markerContext,\n }\n}\n","import type {\n AnnotatedScreenshotResult,\n ScreenCapturePort,\n ScreenshotResult,\n} from \"../types\"\nimport { captureAnnotatedViewport, captureViewport } from \"../utils/screenshot\"\n\n/**\n * Framework-agnostic service for capturing viewport screenshots.\n */\nexport class ScreenCaptureService implements ScreenCapturePort {\n /**\n * Capture a screenshot of the current viewport.\n * @returns Screenshot result with image data and dimensions\n */\n async capture(): Promise<ScreenshotResult> {\n return captureViewport()\n }\n\n /**\n * Capture an annotated screenshot with marker overlays.\n * Interactive elements are marked with numbered labels.\n * @returns Annotated screenshot result with marker map\n */\n async captureAnnotated(): Promise<AnnotatedScreenshotResult> {\n return captureAnnotatedViewport()\n }\n}\n","import { toError } from \"../utils/error\"\n\nexport type SpeechPlaybackTask = () => Promise<void>\n\ninterface TTSPlaybackQueueOptions {\n onError?: (error: Error) => void\n onPlaybackStart?: () => void\n signal?: AbortSignal\n prepare: (text: string, signal?: AbortSignal) => Promise<SpeechPlaybackTask>\n}\n\n/**\n * Queues sentence-level speech preparation immediately while keeping playback\n * strictly ordered.\n *\n * Preparation is allowed to run ahead of playback so server synthesis can\n * overlap with the currently playing segment, but the returned playback tasks\n * still execute one-by-one in enqueue order.\n */\nexport class TTSPlaybackQueue {\n private error: Error | null = null\n private hasStartedPlayback = false\n private onError?: (error: Error) => void\n private onPlaybackStart?: () => void\n private playbackChain = Promise.resolve()\n private prepare: (\n text: string,\n signal?: AbortSignal,\n ) => Promise<SpeechPlaybackTask>\n private signal?: AbortSignal\n\n constructor(options: TTSPlaybackQueueOptions) {\n this.onError = options.onError\n this.onPlaybackStart = options.onPlaybackStart\n this.prepare = options.prepare\n this.signal = options.signal\n }\n\n /**\n * Queue a speakable text segment.\n */\n enqueue(text: string): void {\n const normalizedText = text.trim()\n if (!normalizedText || this.error || this.signal?.aborted) return\n\n // Kick off preparation immediately so synthesis/download work can overlap\n // with the segment currently playing.\n const preparedPlaybackTask = this.prepare(normalizedText, this.signal)\n\n // Preparation can finish after the queue has already been aborted. Attach\n // a background rejection handler so those late failures are still recorded\n // and do not surface as unhandled promise rejections in tests or apps.\n void preparedPlaybackTask.catch((error) => {\n this.fail(toError(error))\n })\n\n const nextStep = this.playbackChain.then(async () => {\n if (this.signal?.aborted) return\n\n const play = await preparedPlaybackTask\n if (this.signal?.aborted) return\n\n if (!this.hasStartedPlayback) {\n this.hasStartedPlayback = true\n this.onPlaybackStart?.()\n }\n\n await play()\n })\n\n this.playbackChain = nextStep.catch((error) => {\n this.fail(toError(error))\n })\n }\n\n /**\n * Wait until every queued segment has either played or the queue failed.\n */\n async waitForCompletion(): Promise<void> {\n await this.playbackChain\n\n if (this.error) {\n throw this.error\n }\n }\n\n private fail(error: Error): void {\n if (this.error) return\n\n this.error = error\n this.onError?.(error)\n }\n}\n","/**\n * Audio conversion utilities for voice capture.\n * Converts Float32 audio data to WAV format for server transcription.\n */\n\n/**\n * Merge multiple Float32Array chunks into a single array\n */\nexport function mergeAudioChunks(chunks: Float32Array[]): Float32Array {\n const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0)\n const result = new Float32Array(totalLength)\n\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.length\n }\n\n return result\n}\n\n/**\n * Convert Float32 audio data to 16-bit PCM\n */\nfunction floatTo16BitPCM(\n output: DataView,\n offset: number,\n input: Float32Array,\n): void {\n for (let i = 0; i < input.length; i++, offset += 2) {\n const sample = Math.max(-1, Math.min(1, input[i]))\n output.setInt16(\n offset,\n sample < 0 ? sample * 0x8000 : sample * 0x7fff,\n true,\n )\n }\n}\n\n/**\n * Write a string to a DataView\n */\nfunction writeString(view: DataView, offset: number, string: string): void {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i))\n }\n}\n\n/**\n * Encode Float32 audio data as a WAV file\n */\nexport function encodeWAV(samples: Float32Array, sampleRate: number): Blob {\n const numChannels = 1\n const bitsPerSample = 16\n const bytesPerSample = bitsPerSample / 8\n const blockAlign = numChannels * bytesPerSample\n\n const dataLength = samples.length * bytesPerSample\n const buffer = new ArrayBuffer(44 + dataLength)\n const view = new DataView(buffer)\n\n // RIFF header\n writeString(view, 0, \"RIFF\")\n view.setUint32(4, 36 + dataLength, true)\n writeString(view, 8, \"WAVE\")\n\n // fmt chunk\n writeString(view, 12, \"fmt \")\n view.setUint32(16, 16, true) // chunk size\n view.setUint16(20, 1, true) // audio format (PCM)\n view.setUint16(22, numChannels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, sampleRate * blockAlign, true) // byte rate\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n\n // data chunk\n writeString(view, 36, \"data\")\n view.setUint32(40, dataLength, true)\n\n floatTo16BitPCM(view, 44, samples)\n\n return new Blob([buffer], { type: \"audio/wav\" })\n}\n","/**\n * AudioWorklet processor code for voice capture.\n * Inlined as a blob URL to avoid separate file serving requirements.\n */\nconst workletCode = `\nclass AudioCaptureProcessor extends AudioWorkletProcessor {\n constructor() {\n super()\n this.isRecording = true\n this.audioChunkSize = 2048\n this.audioBuffer = new Float32Array(this.audioChunkSize)\n this.audioBufferIndex = 0\n this.levelFramesPerUpdate = 4\n this.levelFrameCount = 0\n this.levelRmsSum = 0\n this.levelPeak = 0\n\n this.port.onmessage = (event) => {\n if (event.data?.type === \"flush\") {\n this.flushAudio()\n this.flushLevel()\n this.port.postMessage({ type: \"flush-complete\" })\n }\n }\n }\n\n flushAudio() {\n if (this.audioBufferIndex === 0) return\n\n const chunk = this.audioBuffer.slice(0, this.audioBufferIndex)\n this.port.postMessage({\n type: \"audio\",\n data: chunk\n })\n this.audioBufferIndex = 0\n }\n\n flushLevel() {\n if (this.levelFrameCount === 0) return\n\n this.port.postMessage({\n type: \"level\",\n rms: this.levelRmsSum / this.levelFrameCount,\n peak: this.levelPeak\n })\n\n this.levelFrameCount = 0\n this.levelRmsSum = 0\n this.levelPeak = 0\n }\n\n process(inputs) {\n if (!this.isRecording) return false\n\n const input = inputs[0]\n if (input && input.length > 0) {\n const channelData = input[0]\n let sum = 0\n let peak = 0\n for (let i = 0; i < channelData.length; i++) {\n const sample = channelData[i]\n sum += sample * sample\n const absolute = Math.abs(sample)\n if (absolute > peak) peak = absolute\n }\n\n this.levelRmsSum += Math.sqrt(sum / channelData.length)\n this.levelPeak = Math.max(this.levelPeak, peak)\n this.levelFrameCount += 1\n\n if (this.levelFrameCount >= this.levelFramesPerUpdate) {\n this.flushLevel()\n }\n\n let readOffset = 0\n while (readOffset < channelData.length) {\n const remaining = this.audioBuffer.length - this.audioBufferIndex\n const copyLength = Math.min(remaining, channelData.length - readOffset)\n\n this.audioBuffer.set(\n channelData.subarray(readOffset, readOffset + copyLength),\n this.audioBufferIndex\n )\n\n this.audioBufferIndex += copyLength\n readOffset += copyLength\n\n if (this.audioBufferIndex >= this.audioBuffer.length) {\n this.flushAudio()\n }\n }\n }\n\n return true\n }\n}\n\nregisterProcessor(\"audio-capture-processor\", AudioCaptureProcessor)\n`\n\nlet cachedBlobURL: string | null = null\n\n/**\n * Create a blob URL for the audio worklet processor.\n * Caches the URL to avoid creating multiple blobs.\n */\nexport function createWorkletBlobURL(): string {\n if (!cachedBlobURL) {\n const blob = new Blob([workletCode], { type: \"application/javascript\" })\n cachedBlobURL = URL.createObjectURL(blob)\n }\n return cachedBlobURL\n}\n\n/**\n * Clean up the cached worklet blob URL.\n * Call this when the app unmounts if needed.\n */\nexport function revokeWorkletBlobURL(): void {\n if (cachedBlobURL) {\n URL.revokeObjectURL(cachedBlobURL)\n cachedBlobURL = null\n }\n}\n","import type { VoiceCapturePort } from \"../types\"\nimport { encodeWAV, mergeAudioChunks } from \"../utils/audio\"\nimport { createWorkletBlobURL } from \"../utils/audio-worklet\"\n\nconst SAMPLE_RATE = 16000\nconst AUDIO_LEVEL_NOISE_GATE = 0.0005\nconst AUDIO_LEVEL_INPUT_GAIN = 600\nconst AUDIO_LEVEL_ATTACK = 0.7\nconst AUDIO_LEVEL_RELEASE = 0.25\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\nfunction normalizeAudioLevel(rms: number): number {\n const gatedRms = Math.max(0, rms - AUDIO_LEVEL_NOISE_GATE)\n return clamp(\n Math.log1p(gatedRms * AUDIO_LEVEL_INPUT_GAIN) /\n Math.log1p(AUDIO_LEVEL_INPUT_GAIN),\n 0,\n 1,\n )\n}\n\nfunction smoothAudioLevel(current: number, target: number): number {\n const smoothing = target > current ? AUDIO_LEVEL_ATTACK : AUDIO_LEVEL_RELEASE\n return current + (target - current) * smoothing\n}\n\n/**\n * Framework-agnostic service for voice capture using AudioWorkletNode.\n */\nexport class VoiceCaptureService implements VoiceCapturePort {\n private audioContext: AudioContext | null = null\n private workletNode: AudioWorkletNode | null = null\n private sourceNode: MediaStreamAudioSourceNode | null = null\n private silentGainNode: GainNode | null = null\n private stream: MediaStream | null = null\n private chunks: Float32Array[] = []\n private levelCallback: ((level: number) => void) | null = null\n private visualLevel = 0\n private flushResolve: (() => void) | null = null\n\n /**\n * Register a callback to receive audio level updates (0-1).\n * Called at ~60fps during recording for waveform visualization.\n */\n onLevel(callback: (level: number) => void): void {\n this.levelCallback = callback\n }\n\n /**\n * Start recording audio from the microphone.\n * @throws Error if microphone access is denied\n */\n async start(): Promise<void> {\n this.chunks = []\n this.visualLevel = 0\n\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n sampleRate: SAMPLE_RATE,\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n },\n })\n this.stream = stream\n\n const audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })\n this.audioContext = audioContext\n await audioContext.resume()\n\n // Load worklet from blob URL\n const workletURL = createWorkletBlobURL()\n await audioContext.audioWorklet.addModule(workletURL)\n\n const source = audioContext.createMediaStreamSource(stream)\n this.sourceNode = source\n const workletNode = new AudioWorkletNode(\n audioContext,\n \"audio-capture-processor\",\n )\n this.workletNode = workletNode\n const silentGainNode = audioContext.createGain()\n silentGainNode.gain.value = 0\n this.silentGainNode = silentGainNode\n\n workletNode.port.onmessage = (event) => {\n const { type, data, rms, peak } = event.data\n\n if (type === \"audio\") {\n this.chunks.push(data)\n } else if (type === \"level\" && this.levelCallback) {\n const signalLevel = Math.max(rms ?? 0, (peak ?? 0) * 0.6)\n const targetLevel = normalizeAudioLevel(signalLevel)\n this.visualLevel = smoothAudioLevel(this.visualLevel, targetLevel)\n this.levelCallback(this.visualLevel)\n } else if (type === \"flush-complete\") {\n this.flushResolve?.()\n this.flushResolve = null\n }\n }\n\n source.connect(workletNode)\n workletNode.connect(silentGainNode)\n silentGainNode.connect(audioContext.destination)\n }\n\n /**\n * Stop recording and return the captured audio as a WAV blob.\n */\n async stop(): Promise<Blob> {\n await this.flushPendingAudio()\n\n // Stop the media stream tracks\n if (this.stream) {\n this.stream.getTracks().forEach((track) => track.stop())\n this.stream = null\n }\n\n // Disconnect and close audio nodes\n if (this.sourceNode) {\n this.sourceNode.disconnect()\n this.sourceNode = null\n }\n\n if (this.workletNode) {\n this.workletNode.disconnect()\n this.workletNode = null\n }\n\n if (this.silentGainNode) {\n this.silentGainNode.disconnect()\n this.silentGainNode = null\n }\n\n if (this.audioContext) {\n await this.audioContext.close()\n this.audioContext = null\n }\n\n // Reset audio level\n this.visualLevel = 0\n this.levelCallback?.(0)\n\n // Encode captured audio as WAV\n const audioData = mergeAudioChunks(this.chunks)\n const wavBlob = encodeWAV(audioData, SAMPLE_RATE)\n\n this.chunks = []\n\n return wavBlob\n }\n\n /**\n * Clean up all resources.\n *\n * The level callback is intentionally preserved so the same service instance\n * can be reused across multiple push-to-talk turns without re-registering\n * the waveform subscription from the client.\n */\n dispose(): void {\n if (this.stream) {\n this.stream.getTracks().forEach((track) => track.stop())\n this.stream = null\n }\n if (this.sourceNode) {\n this.sourceNode.disconnect()\n this.sourceNode = null\n }\n if (this.workletNode) {\n this.workletNode.disconnect()\n this.workletNode = null\n }\n if (this.silentGainNode) {\n this.silentGainNode.disconnect()\n this.silentGainNode = null\n }\n if (this.audioContext) {\n this.audioContext.close()\n this.audioContext = null\n }\n this.chunks = []\n this.visualLevel = 0\n this.levelCallback?.(0)\n this.flushResolve = null\n }\n\n private async flushPendingAudio(): Promise<void> {\n if (!this.workletNode) return\n\n await new Promise<void>((resolve) => {\n const timeoutId = setTimeout(() => {\n this.flushResolve = null\n resolve()\n }, 50)\n\n this.flushResolve = () => {\n clearTimeout(timeoutId)\n resolve()\n }\n\n this.workletNode?.port.postMessage({ type: \"flush\" })\n })\n }\n}\n","import type { VoiceEvent, VoiceState } from \"./types\"\n\n/**\n * State transition table for the voice interaction flow.\n * Maps current state + event type to next state.\n */\nconst transitions: Record<\n VoiceState,\n Partial<Record<VoiceEvent[\"type\"], VoiceState>>\n> = {\n idle: {\n HOTKEY_PRESSED: \"listening\",\n },\n listening: {\n HOTKEY_RELEASED: \"processing\",\n ERROR: \"idle\",\n },\n processing: {\n RESPONSE_STARTED: \"responding\",\n TTS_COMPLETE: \"idle\",\n HOTKEY_PRESSED: \"listening\", // Interruption\n ERROR: \"idle\",\n },\n responding: {\n TTS_COMPLETE: \"idle\",\n HOTKEY_PRESSED: \"listening\", // Interruption\n ERROR: \"idle\",\n },\n}\n\nexport interface StateMachine {\n /** Get current state */\n getState(): VoiceState\n /** Attempt a state transition. Returns true if transition was valid. */\n transition(event: VoiceEvent): boolean\n /** Subscribe to state changes */\n subscribe(listener: () => void): () => void\n /** Reset to idle state */\n reset(): void\n}\n\n/**\n * Create a simple typed state machine for the voice interaction flow.\n *\n * States: idle -> listening -> processing -> responding -> idle\n *\n * Supports interruption: pressing hotkey during processing or responding\n * immediately transitions back to listening.\n */\nexport function createStateMachine(initial: VoiceState = \"idle\"): StateMachine {\n let state = initial\n const listeners = new Set<() => void>()\n\n function notify() {\n listeners.forEach((listener) => listener())\n }\n\n return {\n getState: () => state,\n\n transition: (event: VoiceEvent): boolean => {\n const nextState = transitions[state][event.type]\n if (!nextState) return false\n\n state = nextState\n notify()\n return true\n },\n\n subscribe: (listener: () => void) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n reset: () => {\n state = \"idle\"\n notify()\n },\n }\n}\n","import type { PointToolInput } from \"../../shared/point-tool\"\n\n/**\n * Parsed chunk from AI SDK UI message stream.\n */\nexport type UIStreamChunk =\n | { type: \"text-delta\"; delta: string }\n | { type: \"tool-input-available\"; toolName: string; input: unknown }\n | { type: \"finish\" }\n | { type: \"error\"; errorText: string }\n | { type: \"unknown\" }\n\n/**\n * Parse a single line from the UI message stream.\n * The stream format is SSE with \"data: \" prefix followed by JSON.\n */\nexport function parseUIStreamLine(line: string): UIStreamChunk | null {\n const trimmed = line.trim()\n if (!trimmed) return null\n\n // Handle SSE format: strip \"data: \" prefix\n let jsonStr = trimmed\n if (trimmed.startsWith(\"data: \")) {\n jsonStr = trimmed.slice(6)\n }\n\n // Skip [DONE] marker\n if (jsonStr === \"[DONE]\") return null\n\n try {\n const chunk = JSON.parse(jsonStr) as {\n type: string\n delta?: string\n toolName?: string\n input?: unknown\n errorText?: string\n }\n\n switch (chunk.type) {\n case \"text-delta\":\n return { type: \"text-delta\", delta: chunk.delta ?? \"\" }\n\n case \"tool-input-available\":\n return {\n type: \"tool-input-available\",\n toolName: chunk.toolName ?? \"\",\n input: chunk.input,\n }\n\n case \"finish\":\n return { type: \"finish\" }\n\n case \"error\":\n return { type: \"error\", errorText: chunk.errorText ?? \"Unknown error\" }\n\n default:\n return { type: \"unknown\" }\n }\n } catch {\n return null\n }\n}\n\n/**\n * Check if a tool call is a point tool call with valid input.\n */\nexport function isPointToolCall(\n chunk: UIStreamChunk,\n): chunk is {\n type: \"tool-input-available\"\n toolName: \"point\"\n input: PointToolInput\n} {\n return (\n chunk.type === \"tool-input-available\" &&\n chunk.toolName === \"point\" &&\n chunk.input != null &&\n typeof chunk.input === \"object\" &&\n \"type\" in chunk.input &&\n \"label\" in chunk.input\n )\n}\n","import type { PointToolInput } from \"../../shared/point-tool\"\nimport { isPointToolCall, parseUIStreamLine } from \"./ui-stream-parser\"\n\nconst COMMON_ABBREVIATIONS = [\n \"mr.\",\n \"mrs.\",\n \"ms.\",\n \"dr.\",\n \"prof.\",\n \"sr.\",\n \"jr.\",\n \"e.g.\",\n \"i.e.\",\n]\nconst CLOSING_PUNCTUATION = new Set(['\"', \"'\", \"\\u201D\", \"\\u2019\", \")\", \"]\", \"}\"])\nconst SHORT_SEGMENT_THRESHOLD = 24\n\nfunction isLikelySentenceBoundary(text: string, index: number): boolean {\n const char = text[index]\n if (char === \"!\" || char === \"?\" || char === \"…\" || char === \"\\n\") {\n return true\n }\n\n if (char !== \".\") return false\n\n const previousChar = text[index - 1] ?? \"\"\n const nextChar = text[index + 1] ?? \"\"\n\n if (/\\d/.test(previousChar) && /\\d/.test(nextChar)) {\n return false\n }\n\n const lookback = text.slice(Math.max(0, index - 10), index + 1).toLowerCase()\n if (\n COMMON_ABBREVIATIONS.some((abbreviation) => lookback.endsWith(abbreviation))\n ) {\n return false\n }\n\n return true\n}\n\nfunction findBoundaryEnd(text: string, start: number): number | null {\n for (let index = start; index < text.length; index++) {\n const char = text[index]\n\n if (char === \"\\n\") {\n let end = index + 1\n while (end < text.length && /\\s/.test(text[end] ?? \"\")) {\n end++\n }\n return end\n }\n\n if (!isLikelySentenceBoundary(text, index)) continue\n\n let end = index + 1\n while (end < text.length && CLOSING_PUNCTUATION.has(text[end] ?? \"\")) {\n end++\n }\n\n if (end < text.length) {\n const nextChar = text[end] ?? \"\"\n if (!/\\s/.test(nextChar) && !/[A-Z0-9]/.test(nextChar)) {\n continue\n }\n }\n\n while (end < text.length && /\\s/.test(text[end] ?? \"\")) {\n end++\n }\n\n return end\n }\n\n return null\n}\n\nfunction extractCompletedSegments(text: string): {\n consumedLength: number\n segments: string[]\n} {\n const segments: string[] = []\n let consumedLength = 0\n\n while (consumedLength < text.length) {\n const boundaryEnd = findBoundaryEnd(text, consumedLength)\n if (boundaryEnd === null) break\n\n const segment = text.slice(consumedLength, boundaryEnd).trim()\n if (segment) {\n segments.push(segment)\n }\n\n consumedLength = boundaryEnd\n }\n\n return { consumedLength, segments }\n}\n\nexport interface ProcessedResponseChunk {\n speechSegments: string[]\n visibleText: string\n pointToolCall: PointToolInput | null\n}\n\nexport interface FinalProcessedResponse {\n finalResponseText: string\n speechSegments: string[]\n pointToolCall: PointToolInput | null\n}\n\n/**\n * Processes a streaming AI SDK UI message stream response.\n * Extracts text for display/TTS and captures point tool calls.\n */\nexport class ProgressiveResponseProcessor {\n private consumedTextLength = 0\n private pendingShortSegment = \"\"\n private rawText = \"\"\n private buffer = \"\"\n private pointToolCall: PointToolInput | null = null\n\n /**\n * Push raw stream data and extract text chunks and tool calls.\n * The UI message stream format is newline-delimited JSON.\n */\n push(chunk: string): ProcessedResponseChunk {\n this.buffer += chunk\n const lines = this.buffer.split(\"\\n\")\n\n // Keep incomplete last line in buffer\n this.buffer = lines.pop() ?? \"\"\n\n const newTextParts: string[] = []\n\n for (const line of lines) {\n const parsed = parseUIStreamLine(line)\n if (!parsed) continue\n\n if (parsed.type === \"text-delta\") {\n newTextParts.push(parsed.delta)\n } else if (isPointToolCall(parsed)) {\n // Capture first point tool call only\n if (!this.pointToolCall) {\n this.pointToolCall = parsed.input\n }\n }\n }\n\n // Accumulate new text\n if (newTextParts.length > 0) {\n this.rawText += newTextParts.join(\"\")\n }\n\n // Extract completed sentences for TTS\n const unprocessedText = this.rawText.slice(this.consumedTextLength)\n const { consumedLength, segments } = extractCompletedSegments(unprocessedText)\n this.consumedTextLength += consumedLength\n\n return {\n visibleText: this.rawText,\n speechSegments: this.coalesceSegments(segments),\n pointToolCall: this.pointToolCall,\n }\n }\n\n /**\n * Finalize processing and return any remaining text/tool call.\n */\n finish(): FinalProcessedResponse {\n // Process any remaining buffer\n if (this.buffer) {\n const parsed = parseUIStreamLine(this.buffer)\n if (parsed?.type === \"text-delta\") {\n this.rawText += parsed.delta\n } else if (parsed && isPointToolCall(parsed) && !this.pointToolCall) {\n this.pointToolCall = parsed.input\n }\n this.buffer = \"\"\n }\n\n const trailingText = this.rawText.slice(this.consumedTextLength).trim()\n const finalSegmentParts = [this.pendingShortSegment, trailingText].filter(\n Boolean,\n )\n\n this.pendingShortSegment = \"\"\n\n return {\n finalResponseText: this.rawText.trim(),\n speechSegments: finalSegmentParts.length\n ? [finalSegmentParts.join(\" \").trim()]\n : [],\n pointToolCall: this.pointToolCall,\n }\n }\n\n private coalesceSegments(segments: string[]): string[] {\n const speechSegments: string[] = []\n\n for (const segment of segments) {\n const normalizedSegment = segment.trim()\n if (!normalizedSegment) continue\n\n const candidate = this.pendingShortSegment\n ? `${this.pendingShortSegment} ${normalizedSegment}`\n : normalizedSegment\n\n if (candidate.length < SHORT_SEGMENT_THRESHOLD) {\n this.pendingShortSegment = candidate\n continue\n }\n\n this.pendingShortSegment = \"\"\n speechSegments.push(candidate)\n }\n\n return speechSegments\n }\n}\n","import type { PointToolInput } from \"../shared/point-tool\"\nimport { $audioLevel, $conversationHistory, $isEnabled } from \"./atoms\"\nimport { AudioPlaybackService } from \"./services/audio-playback\"\nimport { BrowserSpeechService } from \"./services/browser-speech\"\nimport { LiveTranscriptionService } from \"./services/live-transcription\"\nimport { PointerController } from \"./services/pointer-controller\"\nimport { ScreenCaptureService } from \"./services/screen-capture\"\nimport {\n type SpeechPlaybackTask,\n TTSPlaybackQueue,\n} from \"./services/tts-playback-queue\"\nimport { VoiceCaptureService } from \"./services/voice-capture\"\nimport { createStateMachine, type StateMachine } from \"./state-machine\"\nimport type {\n AnnotatedScreenshotResult,\n AudioPlaybackPort,\n BrowserSpeechPort,\n ConversationMessage,\n CursorBuddyClientOptions,\n CursorBuddyMediaMode,\n CursorBuddyServices,\n CursorBuddySnapshot,\n LiveTranscriptionPort,\n PointerControllerPort,\n PointingTarget,\n ScreenCapturePort,\n VoiceCapturePort,\n} from \"./types\"\nimport { resolveMarkerToCoordinates } from \"./utils/elements\"\nimport { toError } from \"./utils/error\"\nimport { ProgressiveResponseProcessor } from \"./utils/response-processor\"\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\nasync function readErrorMessage(\n response: Response,\n fallbackMessage: string,\n): Promise<string> {\n try {\n // Prefer structured JSON errors from our handlers, but degrade gracefully\n // to plain text if the route returns a different payload.\n const contentType = response.headers.get(\"Content-Type\") ?? \"\"\n\n if (contentType.includes(\"application/json\")) {\n const body = (await response.json()) as { error?: string }\n if (body?.error) return body.error\n }\n\n const text = await response.text()\n if (text) return text\n } catch {\n // Fall back to the generic message when the response body cannot be read.\n }\n\n return fallbackMessage\n}\n\n/**\n * Map coordinate-based pointing from screenshot space to viewport space.\n */\nfunction mapCoordinatesToViewport(\n x: number,\n y: number,\n screenshot: AnnotatedScreenshotResult,\n): { x: number; y: number } {\n if (screenshot.width <= 0 || screenshot.height <= 0) {\n return { x, y }\n }\n\n const scaleX = screenshot.viewportWidth / screenshot.width\n const scaleY = screenshot.viewportHeight / screenshot.height\n\n return {\n x: clamp(\n Math.round(x * scaleX),\n 0,\n Math.max(screenshot.viewportWidth - 1, 0),\n ),\n y: clamp(\n Math.round(y * scaleY),\n 0,\n Math.max(screenshot.viewportHeight - 1, 0),\n ),\n }\n}\n\nexport type { CursorBuddyServices } from \"./types\"\n\n/**\n * Framework-agnostic client for cursor buddy voice interactions.\n *\n * Manages the complete voice interaction flow:\n * idle -> listening -> processing -> responding -> idle\n *\n * Supports interruption: pressing hotkey during any state aborts\n * in-flight work and immediately transitions to listening.\n */\nexport class CursorBuddyClient {\n private endpoint: string\n private options: CursorBuddyClientOptions\n\n // Services\n private voiceCapture: VoiceCapturePort\n private audioPlayback: AudioPlaybackPort\n private browserSpeech: BrowserSpeechPort\n private liveTranscription: LiveTranscriptionPort\n private screenCapture: ScreenCapturePort\n private pointerController: PointerControllerPort\n private stateMachine: StateMachine\n\n // State\n private liveTranscript = \"\"\n private transcript = \"\"\n private response = \"\"\n private error: Error | null = null\n private abortController: AbortController | null = null\n private historyCommittedForTurn = false\n private speechProviderForTurn: \"browser\" | \"server\" | null = null\n private screenshotPromise: Promise<AnnotatedScreenshotResult> | null = null\n\n // Cached snapshot for useSyncExternalStore (must be referentially stable)\n private cachedSnapshot: CursorBuddySnapshot\n\n // Subscriptions\n private listeners = new Set<() => void>()\n\n constructor(\n endpoint: string,\n options: CursorBuddyClientOptions = {},\n services: CursorBuddyServices = {},\n ) {\n this.endpoint = endpoint\n this.options = options\n\n // Initialize services (allow injection for testing)\n this.voiceCapture = services.voiceCapture ?? new VoiceCaptureService()\n this.audioPlayback = services.audioPlayback ?? new AudioPlaybackService()\n this.browserSpeech = services.browserSpeech ?? new BrowserSpeechService()\n this.liveTranscription =\n services.liveTranscription ?? new LiveTranscriptionService()\n this.screenCapture = services.screenCapture ?? new ScreenCaptureService()\n this.pointerController =\n services.pointerController ?? new PointerController()\n this.stateMachine = createStateMachine()\n\n // Initialize cached snapshot\n this.cachedSnapshot = this.buildSnapshot()\n\n // Wire up audio level to atom\n this.voiceCapture.onLevel((level) => $audioLevel.set(level))\n this.liveTranscription.onPartial((text) => {\n if (this.liveTranscript === text) return\n this.liveTranscript = text\n this.notify()\n })\n\n // Wire up state machine changes\n this.stateMachine.subscribe(() => {\n this.options.onStateChange?.(this.stateMachine.getState())\n this.notify()\n })\n\n // Wire up pointer controller\n this.pointerController.subscribe(() => this.notify())\n }\n\n // === Public API ===\n\n /**\n * Start listening for voice input.\n * Aborts any in-flight work from previous session.\n */\n startListening(): void {\n // 1. Abort previous session synchronously\n this.abort()\n\n // 2. Clear UI state immediately\n this.liveTranscript = \"\"\n this.transcript = \"\"\n this.response = \"\"\n this.error = null\n this.historyCommittedForTurn = false\n this.speechProviderForTurn = null\n this.pointerController.release()\n\n // 3. Transition state\n this.stateMachine.transition({ type: \"HOTKEY_PRESSED\" })\n this.notify()\n\n // 4. Start mic (async, errors go to error state)\n this.abortController = new AbortController()\n const signal = this.abortController.signal\n\n // 5. Screenshot is captured in parallel with voice input to reduce latency\n this.screenshotPromise = this.screenCapture.captureAnnotated()\n\n // 6. Start mic (async, errors go to error state)\n this.beginListeningSession(signal).catch((error) => {\n if (signal.aborted) return\n\n this.voiceCapture.dispose()\n this.liveTranscription.dispose()\n this.handleError(toError(error, \"Failed to start listening\"))\n })\n }\n\n /**\n * Stop listening and process the voice input.\n */\n async stopListening(): Promise<void> {\n if (this.stateMachine.getState() !== \"listening\") return\n\n this.stateMachine.transition({ type: \"HOTKEY_RELEASED\" })\n const signal = this.abortController?.signal\n let turnFailure: Error | null = null\n\n const failTurn = (error: Error) => {\n if (turnFailure || signal?.aborted) return\n\n turnFailure = error\n this.audioPlayback.stop()\n this.browserSpeech.stop()\n this.abortController?.abort()\n }\n\n try {\n const [audioBlob, browserTranscript] = await Promise.all([\n this.voiceCapture.stop(),\n this.stopLiveTranscription(),\n ])\n\n let screenshot: AnnotatedScreenshotResult\n try {\n if (!this.screenshotPromise) {\n throw new Error(\"Screenshot was not started\")\n }\n screenshot = await this.screenshotPromise\n } catch (screenshotError) {\n const errorMessage =\n screenshotError instanceof Error\n ? `Failed to capture screenshot: ${screenshotError.message}`\n : \"Failed to capture screenshot\"\n throw new Error(errorMessage)\n }\n\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n // Resolve transcript from browser or server fallback\n const transcript = await this.resolveTranscript(\n browserTranscript,\n audioBlob,\n signal,\n )\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n this.liveTranscript = \"\"\n this.transcript = transcript\n this.options.onTranscript?.(transcript)\n this.notify()\n\n this.prepareSpeechMode()\n\n // Chat stream + progressive sentence TTS\n const { cleanResponse, pointToolCall, playbackQueue } =\n await this.chatAndSpeak(transcript, screenshot, signal, {\n onFailure: failTurn,\n onPlaybackStart: () => {\n this.stateMachine.transition({ type: \"RESPONSE_STARTED\" })\n },\n })\n\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n this.options.onResponse?.(cleanResponse)\n\n // Resolve pointing target from tool call (marker-based or coordinate-based)\n let pointTarget: PointingTarget | null = null\n\n if (pointToolCall) {\n if (pointToolCall.type === \"marker\") {\n // Resolve marker ID to element coordinates\n const coords = resolveMarkerToCoordinates(\n screenshot.markerMap,\n pointToolCall.markerId!,\n )\n if (coords) {\n pointTarget = { ...coords, label: pointToolCall.label }\n }\n } else {\n // Map coordinates from screenshot space to viewport space\n const coords = mapCoordinatesToViewport(\n pointToolCall.x!,\n pointToolCall.y!,\n screenshot,\n )\n pointTarget = { ...coords, label: pointToolCall.label }\n }\n }\n\n // Point if we have a valid target\n if (pointTarget) {\n this.options.onPoint?.(pointTarget)\n this.pointerController.pointAt(pointTarget)\n }\n\n await playbackQueue.waitForCompletion()\n\n if (turnFailure) throw turnFailure\n if (signal?.aborted) return\n\n // Update history only after audio playback succeeds for the full turn\n const history = $conversationHistory.get()\n const newHistory: ConversationMessage[] = [\n ...history,\n { role: \"user\", content: transcript },\n { role: \"assistant\", content: cleanResponse },\n ]\n $conversationHistory.set(newHistory)\n this.historyCommittedForTurn = true\n\n this.stateMachine.transition({ type: \"TTS_COMPLETE\" })\n } catch (err) {\n if (turnFailure) {\n this.handleError(turnFailure)\n return\n }\n // Interruption is not an error\n if (signal?.aborted) return\n this.handleError(toError(err))\n }\n }\n\n /**\n * Enable or disable the buddy.\n */\n setEnabled(enabled: boolean): void {\n $isEnabled.set(enabled)\n this.notify()\n }\n\n /**\n * Manually point at coordinates.\n */\n pointAt(x: number, y: number, label: string): void {\n this.pointerController.pointAt({ x, y, label })\n }\n\n /**\n * Dismiss the current pointing target.\n */\n dismissPointing(): void {\n this.pointerController.release()\n }\n\n /**\n * Reset to idle state and stop any in-progress work.\n */\n reset(): void {\n this.abort()\n this.liveTranscript = \"\"\n this.transcript = \"\"\n this.response = \"\"\n this.error = null\n this.historyCommittedForTurn = false\n this.pointerController.release()\n this.stateMachine.reset()\n this.notify()\n }\n\n /**\n * Update buddy position to follow cursor.\n * Call this on cursor position changes.\n */\n updateCursorPosition(): void {\n this.pointerController.updateFollowPosition()\n }\n\n /**\n * Subscribe to state changes.\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /**\n * Get current state snapshot for React's useSyncExternalStore.\n * Returns a cached object to ensure referential stability.\n */\n getSnapshot(): CursorBuddySnapshot {\n return this.cachedSnapshot\n }\n\n /**\n * Build a new snapshot object.\n */\n private buildSnapshot(): CursorBuddySnapshot {\n return {\n state: this.stateMachine.getState(),\n liveTranscript: this.liveTranscript,\n transcript: this.transcript,\n response: this.response,\n error: this.error,\n isPointing: this.pointerController.isPointing(),\n isEnabled: $isEnabled.get(),\n }\n }\n\n // === Private Methods ===\n\n private abort(): void {\n // Commit partial turn to history if interrupted mid-turn\n this.commitPartialHistory()\n\n this.abortController?.abort()\n this.abortController = null\n this.screenshotPromise = null\n this.voiceCapture.dispose()\n this.liveTranscription.dispose()\n this.audioPlayback.stop()\n this.browserSpeech.stop()\n this.speechProviderForTurn = null\n // Reset audio level on abort\n $audioLevel.set(0)\n }\n\n /**\n * Commit partial turn to history when interrupted.\n * Only commits if we have both transcript and response,\n * and haven't already committed for this turn.\n */\n private commitPartialHistory(): void {\n if (this.historyCommittedForTurn) return\n if (!this.transcript || !this.response) return\n\n const history = $conversationHistory.get()\n const newHistory: ConversationMessage[] = [\n ...history,\n { role: \"user\", content: this.transcript },\n { role: \"assistant\", content: this.response }, // already stripped of POINT tags\n ]\n $conversationHistory.set(newHistory)\n this.historyCommittedForTurn = true\n }\n\n private async transcribe(blob: Blob, signal?: AbortSignal): Promise<string> {\n const formData = new FormData()\n formData.append(\"audio\", blob, \"recording.wav\")\n\n const response = await fetch(`${this.endpoint}/transcribe`, {\n method: \"POST\",\n body: formData,\n signal,\n })\n\n if (!response.ok) {\n throw new Error(await readErrorMessage(response, \"Transcription failed\"))\n }\n\n const { text } = await response.json()\n return text\n }\n\n /**\n * Stream the chat response, keep the visible text updated, and feed complete\n * speech segments into the TTS queue as soon as they are ready.\n */\n private async chatAndSpeak(\n transcript: string,\n screenshot: AnnotatedScreenshotResult,\n signal: AbortSignal | undefined,\n options: {\n onFailure: (error: Error) => void\n onPlaybackStart: () => void\n },\n ): Promise<{\n cleanResponse: string\n pointToolCall: PointToolInput | null\n playbackQueue: TTSPlaybackQueue\n }> {\n const history = $conversationHistory.get()\n\n const response = await fetch(`${this.endpoint}/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n screenshot: screenshot.imageData,\n capture: {\n width: screenshot.width,\n height: screenshot.height,\n },\n transcript,\n history,\n markerContext: screenshot.markerContext,\n }),\n signal,\n })\n\n if (!response.ok) {\n throw new Error(\"Chat request failed\")\n }\n\n const reader = response.body?.getReader()\n if (!reader) throw new Error(\"No response body\")\n\n const decoder = new TextDecoder()\n const responseProcessor = new ProgressiveResponseProcessor()\n const playbackQueue = new TTSPlaybackQueue({\n onError: options.onFailure,\n onPlaybackStart: options.onPlaybackStart,\n prepare: (text, currentSignal) =>\n this.prepareSpeechSegment(text, currentSignal),\n signal,\n })\n const shouldStreamSpeech = this.isSpeechStreamingEnabled()\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n const chunk = decoder.decode(value, { stream: true })\n const { speechSegments, visibleText } = responseProcessor.push(chunk)\n\n if (shouldStreamSpeech) {\n // Queue speech as early as possible, but keep playback ordered so the\n // spoken response stays aligned with the streamed text.\n for (const speechSegment of speechSegments) {\n playbackQueue.enqueue(speechSegment)\n }\n }\n\n this.updateResponse(visibleText)\n }\n\n const trailingChunk = decoder.decode()\n if (trailingChunk) {\n const { speechSegments, visibleText } =\n responseProcessor.push(trailingChunk)\n\n if (shouldStreamSpeech) {\n for (const speechSegment of speechSegments) {\n playbackQueue.enqueue(speechSegment)\n }\n }\n\n this.updateResponse(visibleText)\n }\n\n const finalizedResponse = responseProcessor.finish()\n\n if (shouldStreamSpeech) {\n for (const speechSegment of finalizedResponse.speechSegments) {\n playbackQueue.enqueue(speechSegment)\n }\n } else {\n playbackQueue.enqueue(finalizedResponse.finalResponseText)\n }\n\n this.updateResponse(finalizedResponse.finalResponseText)\n\n return {\n cleanResponse: finalizedResponse.finalResponseText,\n pointToolCall: finalizedResponse.pointToolCall,\n playbackQueue,\n }\n }\n\n /**\n * Request server-side TTS audio for one text segment.\n */\n private async synthesizeSpeech(\n text: string,\n signal?: AbortSignal,\n ): Promise<Blob> {\n const response = await fetch(`${this.endpoint}/tts`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text }),\n signal,\n })\n\n if (!response.ok) {\n throw new Error(await readErrorMessage(response, \"TTS request failed\"))\n }\n\n return response.blob()\n }\n\n /**\n * Resolve the initial speech provider for this turn.\n *\n * Decision tree:\n * 1. In `server` mode, always synthesize on the server.\n * 2. In `browser` mode, require browser speech support up front.\n * 3. In `auto` mode, prefer browser speech when available and keep that\n * choice cached so later segments stay on the same provider unless a\n * browser failure forces a one-way fallback to the server.\n */\n private prepareSpeechMode(): void {\n const speechMode = this.getSpeechMode()\n\n if (speechMode === \"browser\" && !this.browserSpeech.isAvailable()) {\n throw new Error(\"Browser speech is not supported\")\n }\n\n if (speechMode === \"server\") {\n this.speechProviderForTurn = \"server\"\n return\n }\n\n if (speechMode === \"browser\") {\n this.speechProviderForTurn = \"browser\"\n return\n }\n\n this.speechProviderForTurn = this.browserSpeech.isAvailable()\n ? \"browser\"\n : \"server\"\n }\n\n /**\n * Prepare a playback task for one text segment.\n *\n * The queue calls this eagerly so server synthesis can overlap with the\n * currently playing segment, but the returned task is still executed in the\n * original enqueue order.\n */\n private async prepareSpeechSegment(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n switch (this.getSpeechMode()) {\n case \"server\":\n return this.prepareServerSpeechTask(text, signal)\n case \"browser\":\n return this.prepareBrowserSpeechTask(text, signal)\n default:\n return this.prepareAutoSpeechTask(text, signal)\n }\n }\n\n /**\n * Synthesize server audio immediately and return a playback task that reuses\n * the prepared blob later.\n */\n private async prepareServerSpeechTask(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n const blob = await this.synthesizeSpeech(text, signal)\n\n return () => this.audioPlayback.play(blob, signal)\n }\n\n /**\n * Return a browser playback task for one text segment.\n */\n private async prepareBrowserSpeechTask(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n return () => this.browserSpeech.speak(text, signal)\n }\n\n /**\n * Prepare a playback task for `auto` mode.\n *\n * We prefer the browser for low latency, but if browser speech fails for any\n * segment we permanently switch the remainder of the turn to server TTS so\n * later segments do not keep retrying the failing browser path.\n */\n private async prepareAutoSpeechTask(\n text: string,\n signal?: AbortSignal,\n ): Promise<SpeechPlaybackTask> {\n if (this.getAutoSpeechProvider() === \"server\") {\n return this.prepareServerSpeechTask(text, signal)\n }\n\n return async () => {\n // Another segment may already have forced a fallback before this one\n // reaches playback, so re-check the cached provider decision here.\n if (this.getAutoSpeechProvider() === \"server\") {\n const fallbackPlayback = await this.prepareServerSpeechTask(\n text,\n signal,\n )\n await fallbackPlayback()\n return\n }\n\n try {\n await this.browserSpeech.speak(text, signal)\n } catch (error) {\n if (signal?.aborted) return\n\n // Browser speech failed mid-turn. Flip future segments to the server\n // and replay the current segment there so the turn still completes.\n this.speechProviderForTurn = \"server\"\n const fallbackPlayback = await this.prepareServerSpeechTask(\n text,\n signal,\n )\n await fallbackPlayback()\n }\n }\n }\n\n /**\n * Read the current provider choice for `auto` mode, lazily defaulting to the\n * browser when supported and the server otherwise.\n */\n private getAutoSpeechProvider(): \"browser\" | \"server\" {\n if (this.speechProviderForTurn) {\n return this.speechProviderForTurn\n }\n\n this.speechProviderForTurn = this.browserSpeech.isAvailable()\n ? \"browser\"\n : \"server\"\n\n return this.speechProviderForTurn\n }\n\n private handleError(err: Error): void {\n this.liveTranscript = \"\"\n this.error = err\n this.stateMachine.transition({ type: \"ERROR\", error: err })\n this.options.onError?.(err)\n this.notify()\n }\n\n /**\n * Resolve the effective transcription mode for the current client.\n */\n private getTranscriptionMode(): CursorBuddyMediaMode {\n return this.options.transcription?.mode ?? \"auto\"\n }\n\n /**\n * Resolve the effective speech mode for the current client.\n */\n private getSpeechMode(): CursorBuddyMediaMode {\n return this.options.speech?.mode ?? \"server\"\n }\n\n /**\n * Decide whether speech should start before the full chat response is ready.\n */\n private isSpeechStreamingEnabled(): boolean {\n return this.options.speech?.allowStreaming ?? false\n }\n\n /**\n * Decide whether this turn should attempt browser speech recognition.\n */\n private shouldAttemptBrowserTranscription(): boolean {\n return this.getTranscriptionMode() !== \"server\"\n }\n\n /**\n * Decide whether browser speech recognition is mandatory for this turn.\n */\n private isBrowserTranscriptionRequired(): boolean {\n return this.getTranscriptionMode() === \"browser\"\n }\n\n /**\n * Start the recorder and browser speech recognition together.\n *\n * The recorder always runs so we keep waveform updates and preserve a raw\n * audio backup for server fallback in `auto` mode.\n */\n private async beginListeningSession(signal: AbortSignal): Promise<void> {\n const shouldAttemptBrowser = this.shouldAttemptBrowserTranscription()\n const isBrowserTranscriptionAvailable =\n shouldAttemptBrowser && this.liveTranscription.isAvailable()\n\n if (shouldAttemptBrowser && !isBrowserTranscriptionAvailable) {\n if (this.isBrowserTranscriptionRequired()) {\n throw new Error(\"Browser transcription is not supported\")\n }\n }\n\n const [voiceCaptureResult, browserTranscriptionResult] =\n await Promise.allSettled([\n this.voiceCapture.start(),\n isBrowserTranscriptionAvailable\n ? this.liveTranscription.start()\n : Promise.resolve(undefined),\n ])\n\n if (signal.aborted) return\n\n if (voiceCaptureResult.status === \"rejected\") {\n throw toError(voiceCaptureResult.reason, \"Failed to start microphone\")\n }\n\n // In browser-only mode, a browser STT startup failure should fail the turn.\n // In auto mode we silently keep the recorder alive for server fallback.\n if (\n browserTranscriptionResult.status === \"rejected\" &&\n this.isBrowserTranscriptionRequired()\n ) {\n throw toError(\n browserTranscriptionResult.reason,\n \"Browser transcription failed to start\",\n )\n }\n\n if (browserTranscriptionResult.status === \"rejected\") {\n this.liveTranscription.dispose()\n }\n }\n\n /**\n * Stop browser speech recognition and return the best final transcript it\n * produced for this turn.\n */\n private async stopLiveTranscription(): Promise<string> {\n if (\n !this.shouldAttemptBrowserTranscription() ||\n !this.liveTranscription.isAvailable()\n ) {\n return \"\"\n }\n\n try {\n return await this.liveTranscription.stop()\n } catch (error) {\n // Browser mode should surface the recognition error directly.\n // Auto mode falls back to the recorded audio instead.\n if (this.isBrowserTranscriptionRequired()) {\n throw toError(error, \"Browser transcription failed\")\n }\n\n return \"\"\n }\n }\n\n /**\n * Choose the transcript that should drive the turn.\n *\n * Decision tree:\n * 1. Use the browser transcript when it is available.\n * 2. In browser-only mode, fail if the browser produced nothing usable.\n * 3. In auto/server modes, fall back to the recorded audio upload.\n */\n private async resolveTranscript(\n browserTranscript: string,\n audioBlob: Blob,\n signal?: AbortSignal,\n ): Promise<string> {\n const normalizedBrowserTranscript = browserTranscript.trim()\n if (normalizedBrowserTranscript) {\n return normalizedBrowserTranscript\n }\n\n if (this.getTranscriptionMode() === \"browser\") {\n throw new Error(\n \"Browser transcription did not produce a final transcript\",\n )\n }\n\n return this.transcribe(audioBlob, signal)\n }\n\n private updateResponse(text: string): void {\n if (this.response === text) return\n\n this.response = text\n this.notify()\n }\n\n private notify(): void {\n // Update cached snapshot before notifying (required for useSyncExternalStore)\n this.cachedSnapshot = this.buildSnapshot()\n this.listeners.forEach((listener) => listener())\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"point-tool-DtHgq6gQ.mjs","names":[],"sources":["../src/shared/point-tool.ts"],"sourcesContent":["import { tool } from \"ai\"\nimport { z } from \"zod\"\n\nexport const pointToolInputSchema = z\n .object({\n type: z\n .enum([\"marker\", \"coordinates\"])\n .describe(\n \"How to point. Use 'marker' for interactive elements that have a marker. Use 'coordinates' only for visible non-interactive content without a marker.\",\n ),\n\n markerId: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n \"Required when type is 'marker'. The marker ID of the interactive element to point at.\",\n ),\n\n x: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n \"Required when type is 'coordinates'. The horizontal pixel coordinate of the center of the target area.\",\n ),\n\n y: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n \"Required when type is 'coordinates'. The vertical pixel coordinate of the center of the target area.\",\n ),\n\n label: z\n .string()\n .min(1)\n .max(24)\n .describe(\n \"A very short label for the pointer bubble, ideally 2 to 4 words.\",\n ),\n })\n .superRefine((value, ctx) => {\n if (value.type === \"marker\") {\n if (value.markerId == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"markerId\"],\n message: \"markerId is required when type is 'marker'.\",\n })\n }\n\n if (value.x != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"x\"],\n message: \"x must not be provided when type is 'marker'.\",\n })\n }\n\n if (value.y != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"y\"],\n message: \"y must not be provided when type is 'marker'.\",\n })\n }\n }\n\n if (value.type === \"coordinates\") {\n if (value.x == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"x\"],\n message: \"x is required when type is 'coordinates'.\",\n })\n }\n\n if (value.y == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"y\"],\n message: \"y is required when type is 'coordinates'.\",\n })\n }\n\n if (value.markerId != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"markerId\"],\n message: \"markerId must not be provided when type is 'coordinates'.\",\n })\n }\n }\n })\n\nexport type PointToolInput = z.infer<typeof pointToolInputSchema>\n\nexport const pointTool = tool({\n description:\n \"Visually point at something on the user's screen. \" +\n \"Use this tool when the user asks you to locate, indicate, highlight, or show a specific visible target on screen. \" +\n \"Prefer type 'marker' for interactive elements that have a marker. \" +\n \"Use type 'coordinates' only for visible non-interactive content without a marker. \" +\n \"Do not describe a pointing action in plain text instead of calling this tool. \" +\n \"Call this tool at most once per response, and only after your spoken reply.\",\n\n inputSchema: pointToolInputSchema,\n\n execute: async (params) => {\n return `Pointed at \"${params.label}\" on the user's screen.`\n },\n})\n"],"mappings":";;AAsGA,MAAa,YAAY,KAAK;CAC5B,aACE;CAOF,aA5GkC,EACjC,OAAO;EACN,MAAM,EACH,KAAK,CAAC,UAAU,cAAc,CAAC,CAC/B,SACC,uJACD;EAEH,UAAU,EACP,QAAQ,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SACC,wFACD;EAEH,GAAG,EACA,QAAQ,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SACC,yGACD;EAEH,GAAG,EACA,QAAQ,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SACC,uGACD;EAEH,OAAO,EACJ,QAAQ,CACR,IAAI,EAAE,CACN,IAAI,GAAG,CACP,SACC,mEACD;EACJ,CAAC,CACD,aAAa,OAAO,QAAQ;AAC3B,MAAI,MAAM,SAAS,UAAU;AAC3B,OAAI,MAAM,YAAY,KACpB,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,WAAW;IAClB,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;;AAIN,MAAI,MAAM,SAAS,eAAe;AAChC,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,KAAK,KACb,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,IAAI;IACX,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,YAAY,KACpB,KAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,WAAW;IAClB,SAAS;IACV,CAAC;;GAGN;CAeF,SAAS,OAAO,WAAW;AACzB,SAAO,eAAe,OAAO,MAAM;;CAEtC,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-COQKMo5C.d.mts","names":[],"sources":["../src/server/types.ts"],"mappings":";;;;;AAKA;UAAiB,wBAAA;;EAEf,KAAA,EAAO,aAAA;EACP,qBAAA,GAAwB,MAAA;EAMV;;;;EAAd,WAAA,GAAc,WAAA;EAeA;;;;EATd,kBAAA,GAAqB,kBAAA;EANrB;;;;EAYA,MAAA,cAAoB,GAAA;IAAO,aAAA;EAAA;EAG3B;EAAA,KAAA,GAAQ,MAAA,SAAe,IAAA;EAAA;EAGvB,UAAA;AAAA;;AAMF;;UAAiB,kBAAA;EAEI;EAAnB,OAAA,GAAU,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EAAR;EAG/B,MAAA,EAAQ,wBAAA;AAAA"}