cursor-buddy 0.0.2 → 0.0.3
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/dist/{client-DKZY5bI1.d.mts → client-CPQnk2_x.d.mts} +80 -109
- package/dist/client-CPQnk2_x.d.mts.map +1 -0
- package/dist/{client-Bd33JD8T.mjs → client-DAa4L2fE.mjs} +995 -481
- package/dist/client-DAa4L2fE.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react/index.d.mts +21 -21
- package/dist/react/index.d.mts.map +1 -1
- package/dist/react/index.mjs +98 -83
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.mjs +24 -11
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/README.md +0 -344
- package/dist/client-Bd33JD8T.mjs.map +0 -1
- package/dist/client-DKZY5bI1.d.mts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-DAa4L2fE.mjs","names":["clamp"],"sources":["../src/core/atoms.ts","../src/core/pointing.ts","../src/core/services/audio-playback.ts","../src/core/bezier.ts","../src/core/services/pointer-controller.ts","../src/core/utils/elements.ts","../src/core/utils/annotations.ts","../src/core/utils/screenshot.ts","../src/core/services/screen-capture.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/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","import type { PointingTarget, ParsedPointingTag } from \"./types\"\n\n/**\n * Parses POINT tags from AI responses.\n *\n * Supports two formats:\n * - Marker-based: [POINT:5:label] - 3 parts, references a numbered marker\n * - Coordinate-based: [POINT:640,360:label] - 4 parts, raw pixel coordinates\n */\n\n// Matches both formats: [POINT:5:label] or [POINT:640,360:label]\nconst POINTING_TAG_REGEX = /\\[POINT:(\\d+)(?:,(\\d+))?:([^\\]]+)\\]\\s*$/\n\n/**\n * Parse pointing tag into structured result.\n * Returns null if no valid POINT tag is found at the end.\n */\nexport function parsePointingTagRaw(response: string): ParsedPointingTag | null {\n const match = response.match(POINTING_TAG_REGEX)\n if (!match) return null\n\n const first = Number.parseInt(match[1], 10)\n const second = match[2] ? Number.parseInt(match[2], 10) : null\n const label = match[3].trim()\n\n if (second !== null) {\n // Coordinate format: [POINT:x,y:label]\n return { type: \"coordinates\", x: first, y: second, label }\n }\n // Marker format: [POINT:id:label]\n return { type: \"marker\", markerId: first, label }\n}\n\n/**\n * Extract pointing target from response text.\n * For marker-based pointing, returns null (needs resolution via marker map).\n * For coordinate-based pointing, returns the target directly.\n *\n * @deprecated Use parsePointingTagRaw for full marker support\n */\nexport function parsePointingTag(response: string): PointingTarget | null {\n const parsed = parsePointingTagRaw(response)\n if (!parsed) return null\n\n if (parsed.type === \"coordinates\") {\n return { x: parsed.x, y: parsed.y, label: parsed.label }\n }\n\n // Marker-based pointing needs resolution - return null here\n // Client should use parsePointingTagRaw + resolveMarkerToCoordinates\n return null\n}\n\n/**\n * Remove POINT tag from response text for display/TTS.\n */\nexport function stripPointingTag(response: string): string {\n return response.replace(POINTING_TAG_REGEX, \"\").trim()\n}\n","import type { AudioPlaybackPort } from \"../types\"\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\", err instanceof Error ? err : new Error(String(err)))\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","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 { PointingTarget, PointerControllerPort } 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 * 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\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","/**\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","import html2canvas from \"html2canvas-pro\"\nimport type { ScreenshotResult, AnnotatedScreenshotResult } from \"../types\"\nimport { createMarkerMap } from \"./elements\"\nimport { createAnnotatedCanvas, generateMarkerContext } from \"./annotations\"\n\nconst CLONE_RESOURCE_TIMEOUT_MS = 3000\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(captureMetrics: ReturnType<typeof getCaptureMetrics>) {\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 exports as 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(document.body, getHtml2CanvasOptions(captureMetrics))\n } catch {\n canvas = createFallbackCanvas()\n }\n\n return {\n imageData: canvas.toDataURL(\"image/png\"),\n width: canvas.width,\n height: canvas.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 return {\n imageData: canvas.toDataURL(\"image/png\"),\n width: canvas.width,\n height: canvas.height,\n viewportWidth: captureMetrics.viewportWidth,\n viewportHeight: captureMetrics.viewportHeight,\n markerMap,\n markerContext,\n }\n}\n","import type {\n ScreenshotResult,\n AnnotatedScreenshotResult,\n ScreenCapturePort,\n} from \"../types\"\nimport { captureViewport, captureAnnotatedViewport } 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","/**\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 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.flushResolve = null\n this.levelCallback = 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 AI_RESPONSE_COMPLETE: \"responding\",\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 { $audioLevel, $conversationHistory, $isEnabled } from \"./atoms\"\nimport { parsePointingTagRaw, stripPointingTag } from \"./pointing\"\nimport { AudioPlaybackService } from \"./services/audio-playback\"\nimport { PointerController } from \"./services/pointer-controller\"\nimport { ScreenCaptureService } from \"./services/screen-capture\"\nimport { VoiceCaptureService } from \"./services/voice-capture\"\nimport { createStateMachine, type StateMachine } from \"./state-machine\"\nimport type {\n AnnotatedScreenshotResult,\n AudioPlaybackPort,\n ConversationMessage,\n CursorBuddyClientOptions,\n CursorBuddyServices,\n CursorBuddySnapshot,\n PointerControllerPort,\n PointingTarget,\n ScreenCapturePort,\n VoiceCapturePort,\n} from \"./types\"\nimport { resolveMarkerToCoordinates } from \"./utils/elements\"\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\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 screenCapture: ScreenCapturePort\n private pointerController: PointerControllerPort\n private stateMachine: StateMachine\n\n // State\n private transcript = \"\"\n private response = \"\"\n private error: Error | null = null\n private abortController: AbortController | null = null\n private historyCommittedForTurn = false\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.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\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.transcript = \"\"\n this.response = \"\"\n this.error = null\n this.historyCommittedForTurn = false\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 this.voiceCapture.start().catch((err) => this.handleError(err))\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\n try {\n // Stop mic, capture annotated screenshot in parallel\n const [audioBlob, screenshot] = await Promise.all([\n this.voiceCapture.stop(),\n this.screenCapture.captureAnnotated(),\n ])\n\n if (signal?.aborted) return\n\n // Transcribe\n const transcript = await this.transcribe(audioBlob, signal)\n if (signal?.aborted) return\n\n this.transcript = transcript\n this.options.onTranscript?.(transcript)\n this.notify()\n\n // Chat (with marker context)\n const response = await this.chat(transcript, screenshot, signal)\n if (signal?.aborted) return\n\n // Parse pointing tag and strip from response\n const parsed = parsePointingTagRaw(response)\n const cleanResponse = stripPointingTag(response)\n\n this.response = cleanResponse\n this.stateMachine.transition({\n type: \"AI_RESPONSE_COMPLETE\",\n response: cleanResponse,\n })\n this.options.onResponse?.(cleanResponse)\n\n // Update history on successful completion\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 // Resolve pointing target (marker-based or coordinate-based)\n let pointTarget: PointingTarget | null = null\n\n if (parsed) {\n if (parsed.type === \"marker\") {\n // Resolve marker ID to element coordinates\n const coords = resolveMarkerToCoordinates(\n screenshot.markerMap,\n parsed.markerId,\n )\n if (coords) {\n pointTarget = { ...coords, label: parsed.label }\n }\n } else {\n // Map coordinates from screenshot space to viewport space\n const coords = mapCoordinatesToViewport(\n parsed.x,\n parsed.y,\n screenshot,\n )\n pointTarget = { ...coords, label: parsed.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 // TTS\n if (cleanResponse) {\n await this.speak(cleanResponse, signal)\n }\n if (signal?.aborted) return\n\n this.stateMachine.transition({ type: \"TTS_COMPLETE\" })\n } catch (err) {\n // Interruption is not an error\n if (signal?.aborted) return\n this.handleError(err instanceof Error ? err : new Error(\"Unknown error\"))\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.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 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.audioPlayback.stop()\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(\"Transcription failed\")\n }\n\n const { text } = await response.json()\n return text\n }\n\n private async chat(\n transcript: string,\n screenshot: AnnotatedScreenshotResult,\n signal?: AbortSignal,\n ): Promise<string> {\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 // Stream the response\n const reader = response.body?.getReader()\n if (!reader) throw new Error(\"No response body\")\n\n const decoder = new TextDecoder()\n let fullResponse = \"\"\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 fullResponse += chunk\n\n // Update response progressively for UI\n this.response = stripPointingTag(fullResponse)\n this.notify()\n }\n\n return fullResponse\n }\n\n private async speak(text: string, signal?: AbortSignal): Promise<void> {\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(\"TTS request failed\")\n }\n\n const audioBlob = await response.blob()\n await this.audioPlayback.play(audioBlob, signal)\n }\n\n private handleError(err: Error): void {\n this.error = err\n this.stateMachine.transition({ type: \"ERROR\", error: err })\n this.options.onError?.(err)\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.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as Point, c as VoiceEvent, l as VoiceState, n as CursorBuddyClientOptions, o as PointingTarget, r as CursorBuddySnapshot, t as CursorBuddyClient } from "./client-
|
|
1
|
+
import { a as Point, c as VoiceEvent, l as VoiceState, n as CursorBuddyClientOptions, o as PointingTarget, r as CursorBuddySnapshot, t as CursorBuddyClient } from "./client-CPQnk2_x.mjs";
|
|
2
2
|
export { CursorBuddyClient, type CursorBuddyClientOptions, type CursorBuddySnapshot, type Point, type PointingTarget, type VoiceEvent, type VoiceState };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as CursorBuddyClient } from "./client-
|
|
1
|
+
import { t as CursorBuddyClient } from "./client-DAa4L2fE.mjs";
|
|
2
2
|
export { CursorBuddyClient };
|
package/dist/react/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { i as CursorRenderProps, l as VoiceState, n as CursorBuddyClientOptions, o as PointingTarget, s as SpeechBubbleRenderProps, u as WaveformRenderProps } from "../client-
|
|
2
|
+
import { i as CursorRenderProps, l as VoiceState, n as CursorBuddyClientOptions, o as PointingTarget, s as SpeechBubbleRenderProps, u as WaveformRenderProps } from "../client-CPQnk2_x.mjs";
|
|
3
3
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/react/components/Overlay.d.ts
|
|
@@ -69,26 +69,6 @@ declare function CursorBuddy({
|
|
|
69
69
|
onError
|
|
70
70
|
}: CursorBuddyProps): _$react_jsx_runtime0.JSX.Element;
|
|
71
71
|
//#endregion
|
|
72
|
-
//#region src/react/provider.d.ts
|
|
73
|
-
interface CursorBuddyProviderProps extends CursorBuddyClientOptions {
|
|
74
|
-
/** API endpoint for cursor buddy server */
|
|
75
|
-
endpoint: string;
|
|
76
|
-
/** Children */
|
|
77
|
-
children: React.ReactNode;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Provider for cursor buddy. Creates and manages the client instance.
|
|
81
|
-
*/
|
|
82
|
-
declare function CursorBuddyProvider({
|
|
83
|
-
endpoint,
|
|
84
|
-
children,
|
|
85
|
-
onTranscript,
|
|
86
|
-
onResponse,
|
|
87
|
-
onPoint,
|
|
88
|
-
onStateChange,
|
|
89
|
-
onError
|
|
90
|
-
}: CursorBuddyProviderProps): _$react_jsx_runtime0.JSX.Element;
|
|
91
|
-
//#endregion
|
|
92
72
|
//#region src/react/hooks.d.ts
|
|
93
73
|
interface UseCursorBuddyReturn {
|
|
94
74
|
/** Current voice state */
|
|
@@ -123,5 +103,25 @@ interface UseCursorBuddyReturn {
|
|
|
123
103
|
*/
|
|
124
104
|
declare function useCursorBuddy(): UseCursorBuddyReturn;
|
|
125
105
|
//#endregion
|
|
106
|
+
//#region src/react/provider.d.ts
|
|
107
|
+
interface CursorBuddyProviderProps extends CursorBuddyClientOptions {
|
|
108
|
+
/** API endpoint for cursor buddy server */
|
|
109
|
+
endpoint: string;
|
|
110
|
+
/** Children */
|
|
111
|
+
children: React.ReactNode;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Provider for cursor buddy. Creates and manages the client instance.
|
|
115
|
+
*/
|
|
116
|
+
declare function CursorBuddyProvider({
|
|
117
|
+
endpoint,
|
|
118
|
+
children,
|
|
119
|
+
onTranscript,
|
|
120
|
+
onResponse,
|
|
121
|
+
onPoint,
|
|
122
|
+
onStateChange,
|
|
123
|
+
onError
|
|
124
|
+
}: CursorBuddyProviderProps): _$react_jsx_runtime0.JSX.Element;
|
|
125
|
+
//#endregion
|
|
126
126
|
export { CursorBuddy, type CursorBuddyProps, CursorBuddyProvider, type CursorBuddyProviderProps, type CursorRenderProps, type SpeechBubbleRenderProps, type UseCursorBuddyReturn, type WaveformRenderProps, useCursorBuddy };
|
|
127
127
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/react/components/Overlay.tsx","../../src/react/components/CursorBuddy.tsx","../../src/react/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/react/components/Overlay.tsx","../../src/react/components/CursorBuddy.tsx","../../src/react/hooks.ts","../../src/react/provider.tsx"],"mappings":";;;;UAsBiB,YAAA;;EAEf,MAAA,GAAS,KAAA,CAAM,SAAA,KAAc,KAAA,EAAO,iBAAA,KAAsB,KAAA,CAAM,SAAA;;EAEhE,YAAA,IAAgB,KAAA,EAAO,uBAAA,KAA4B,KAAA,CAAM,SAAA;EAJ9B;EAM3B,QAAA,IAAY,KAAA,EAAO,mBAAA,KAAwB,KAAA,CAAM,SAAA;EAJxC;EAMT,SAAA,GAAY,WAAA;AAAA;;;UCtBG,gBAAA,SACP,IAAA,CAAK,YAAA;;EAEb,QAAA;EDWe;ECTf,MAAA;;EAEA,SAAA,GAAY,WAAA;EDSwB;ECPpC,YAAA,IAAgB,IAAA;EDSO;ECPvB,UAAA,IAAc,IAAA;EDSK;ECPnB,OAAA,IAAW,MAAA,EAAQ,cAAA;EDSP;ECPZ,aAAA,IAAiB,KAAA,EAAO,UAAA;EDOD;ECLvB,OAAA,IAAW,KAAA,EAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;iBAqDJ,WAAA,CAAA;EACd,QAAA;EACA,MAAA;EACA,SAAA;EACA,MAAA;EACA,YAAA;EACA,QAAA;EACA,YAAA;EACA,UAAA;EACA,OAAA;EACA,aAAA;EACA;AAAA,GACC,gBAAA,GAAgB,oBAAA,CAAA,GAAA,CAAA,OAAA;;;UClFF,oBAAA;;EAEf,KAAA,EAAO,UAAA;;EAEP,UAAA;EFU2B;EER3B,QAAA;EFUS;EERT,UAAA;EFQ0D;EEN1D,SAAA;EFQmD;EENnD,UAAA;EFQ2C;EEN3C,KAAA,EAAO,KAAA;EFQgB;EELvB,cAAA;EFDA;EEGA,aAAA;EFHe;EEKf,UAAA,GAAa,OAAA;EFLgB;EEO7B,OAAA,GAAU,CAAA,UAAW,CAAA,UAAW,KAAA;EFPgC;EEShE,eAAA;EFPuB;EESvB,KAAA;AAAA;;;;iBAMc,cAAA,CAAA,GAAkB,oBAAA;;;UC/BjB,wBAAA,SAAiC,wBAAA;;EAEhD,QAAA;EHUe;EGRf,QAAA,EAAU,KAAA,CAAM,SAAA;AAAA;;;;iBAMF,mBAAA,CAAA;EACd,QAAA;EACA,QAAA;EACA,YAAA;EACA,UAAA;EACA,OAAA;EACA,aAAA;EACA;AAAA,GACC,wBAAA,GAAwB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
package/dist/react/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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-
|
|
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-DAa4L2fE.mjs";
|
|
3
|
+
import { useStore } from "@nanostores/react";
|
|
3
4
|
import { createContext, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { createPortal } from "react-dom";
|
|
6
|
-
import { useStore } from "@nanostores/react";
|
|
7
7
|
//#region src/react/styles.css?inline
|
|
8
|
-
var styles_default = "/**\n * Cursor Buddy Styles\n *\n * Customize by overriding CSS variables in your own stylesheet:\n *\n * :root {\n * --cursor-buddy-color-idle: #8b5cf6;\n * }\n */\n\n:root {\n /* Cursor colors by state */\n --cursor-buddy-color-idle: #3b82f6;\n --cursor-buddy-color-listening: #ef4444;\n --cursor-buddy-color-processing: #eab308;\n --cursor-buddy-color-responding: #22c55e;\n --cursor-buddy-cursor-stroke: #ffffff;\n --cursor-buddy-cursor-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n\n /* Speech bubble */\n --cursor-buddy-bubble-bg: #ffffff;\n --cursor-buddy-bubble-text: #1f2937;\n --cursor-buddy-bubble-radius: 8px;\n --cursor-buddy-bubble-padding: 8px 12px;\n --cursor-buddy-bubble-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n --cursor-buddy-bubble-max-width: 200px;\n --cursor-buddy-bubble-font-size: 14px;\n\n /* Waveform */\n --cursor-buddy-waveform-color: #ef4444;\n --cursor-buddy-waveform-bar-width: 4px;\n --cursor-buddy-waveform-bar-radius: 2px;\n --cursor-buddy-waveform-gap: 3px;\n\n /* Overlay */\n --cursor-buddy-z-index: 2147483647;\n\n /* Animation durations */\n --cursor-buddy-transition-fast: 0.1s;\n --cursor-buddy-transition-normal: 0.2s;\n --cursor-buddy-animation-duration: 0.3s;\n}\n\n/* Overlay container */\n.cursor-buddy-overlay {\n position: fixed;\n inset: 0;\n pointer-events: none;\n isolation: isolate;\n z-index: var(--cursor-buddy-z-index);\n}\n\n/* Buddy container (cursor + accessories) */\n.cursor-buddy-container {\n position: absolute;\n transform: translate(-16px, -
|
|
8
|
+
var styles_default = "/**\n * Cursor Buddy Styles\n *\n * Customize by overriding CSS variables in your own stylesheet:\n *\n * :root {\n * --cursor-buddy-color-idle: #8b5cf6;\n * }\n */\n\n:root {\n /* Cursor colors by state */\n --cursor-buddy-color-idle: #3b82f6;\n --cursor-buddy-color-listening: #ef4444;\n --cursor-buddy-color-processing: #eab308;\n --cursor-buddy-color-responding: #22c55e;\n --cursor-buddy-cursor-stroke: #ffffff;\n --cursor-buddy-cursor-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n\n /* Speech bubble */\n --cursor-buddy-bubble-bg: #ffffff;\n --cursor-buddy-bubble-text: #1f2937;\n --cursor-buddy-bubble-radius: 8px;\n --cursor-buddy-bubble-padding: 8px 12px;\n --cursor-buddy-bubble-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n --cursor-buddy-bubble-max-width: 200px;\n --cursor-buddy-bubble-font-size: 14px;\n\n /* Waveform */\n --cursor-buddy-waveform-color: #ef4444;\n --cursor-buddy-waveform-bar-width: 4px;\n --cursor-buddy-waveform-bar-radius: 2px;\n --cursor-buddy-waveform-gap: 3px;\n\n /* Overlay */\n --cursor-buddy-z-index: 2147483647;\n\n /* Animation durations */\n --cursor-buddy-transition-fast: 0.1s;\n --cursor-buddy-transition-normal: 0.2s;\n --cursor-buddy-animation-duration: 0.3s;\n}\n\n/* Overlay container */\n.cursor-buddy-overlay {\n position: fixed;\n inset: 0;\n pointer-events: none;\n isolation: isolate;\n z-index: var(--cursor-buddy-z-index);\n}\n\n/* Buddy container (cursor + accessories) */\n.cursor-buddy-container {\n position: absolute;\n transform: translate(-16px, -4px);\n}\n\n/* Cursor SVG */\n.cursor-buddy-cursor {\n transition: transform var(--cursor-buddy-transition-fast) ease-out;\n filter: drop-shadow(var(--cursor-buddy-cursor-shadow));\n}\n\n.cursor-buddy-cursor polygon {\n stroke: var(--cursor-buddy-cursor-stroke);\n stroke-width: 2;\n transition: fill var(--cursor-buddy-transition-normal) ease-out;\n}\n\n.cursor-buddy-cursor--idle polygon {\n fill: var(--cursor-buddy-color-idle);\n}\n\n.cursor-buddy-cursor--listening polygon {\n fill: var(--cursor-buddy-color-listening);\n}\n\n.cursor-buddy-cursor--processing polygon {\n fill: var(--cursor-buddy-color-processing);\n}\n\n.cursor-buddy-cursor--responding polygon {\n fill: var(--cursor-buddy-color-responding);\n}\n\n/* Cursor pulse animation during listening */\n.cursor-buddy-cursor--listening {\n animation: cursor-buddy-pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes cursor-buddy-pulse {\n 0%,\n 100% {\n filter: drop-shadow(var(--cursor-buddy-cursor-shadow));\n }\n 50% {\n filter: drop-shadow(0 0 8px var(--cursor-buddy-color-listening));\n }\n}\n\n/* Processing spinner effect */\n.cursor-buddy-cursor--processing {\n animation: cursor-buddy-spin-subtle 2s linear infinite;\n}\n\n@keyframes cursor-buddy-spin-subtle {\n 0% {\n filter: drop-shadow(var(--cursor-buddy-cursor-shadow)) hue-rotate(0deg);\n }\n 100% {\n filter: drop-shadow(var(--cursor-buddy-cursor-shadow)) hue-rotate(360deg);\n }\n}\n\n/* Speech bubble */\n.cursor-buddy-bubble {\n position: absolute;\n left: 40px;\n top: -8px;\n pointer-events: auto;\n cursor: pointer;\n max-width: var(--cursor-buddy-bubble-max-width);\n padding: var(--cursor-buddy-bubble-padding);\n background-color: var(--cursor-buddy-bubble-bg);\n color: var(--cursor-buddy-bubble-text);\n border-radius: var(--cursor-buddy-bubble-radius);\n box-shadow: var(--cursor-buddy-bubble-shadow);\n font-size: var(--cursor-buddy-bubble-font-size);\n line-height: 1.4;\n width: max-content;\n overflow-wrap: break-word;\n word-break: break-word;\n user-select: none;\n animation: cursor-buddy-fade-in var(--cursor-buddy-animation-duration)\n ease-out;\n}\n\n@keyframes cursor-buddy-fade-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Waveform container */\n.cursor-buddy-waveform {\n position: absolute;\n left: 40px;\n top: 4px;\n display: flex;\n align-items: center;\n gap: var(--cursor-buddy-waveform-gap);\n height: 24px;\n animation: cursor-buddy-fade-in var(--cursor-buddy-animation-duration)\n ease-out;\n}\n\n/* Waveform bars */\n.cursor-buddy-waveform-bar {\n width: var(--cursor-buddy-waveform-bar-width);\n background-color: var(--cursor-buddy-waveform-color);\n border-radius: var(--cursor-buddy-waveform-bar-radius);\n transition: height 0.05s ease-out;\n}\n\n/* Fade out animation (applied via JS) */\n.cursor-buddy-fade-out {\n animation: cursor-buddy-fade-out var(--cursor-buddy-animation-duration)\n ease-out forwards;\n}\n\n@keyframes cursor-buddy-fade-out {\n from {\n opacity: 1;\n }\n to {\n opacity: 0;\n }\n}\n";
|
|
9
9
|
//#endregion
|
|
10
10
|
//#region src/react/utils/inject-styles.ts
|
|
11
11
|
const STYLE_ID = "cursor-buddy-styles";
|
|
@@ -95,6 +95,81 @@ function useCursorBuddy() {
|
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
//#endregion
|
|
98
|
+
//#region src/react/use-hotkey.ts
|
|
99
|
+
/**
|
|
100
|
+
* Parse a hotkey string like "ctrl+alt" into modifier flags
|
|
101
|
+
*/
|
|
102
|
+
function parseHotkey(hotkey) {
|
|
103
|
+
const parts = hotkey.toLowerCase().split("+");
|
|
104
|
+
return {
|
|
105
|
+
ctrl: parts.includes("ctrl") || parts.includes("control"),
|
|
106
|
+
alt: parts.includes("alt") || parts.includes("option"),
|
|
107
|
+
shift: parts.includes("shift"),
|
|
108
|
+
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command")
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if a keyboard event matches the required modifiers
|
|
113
|
+
*/
|
|
114
|
+
function matchesHotkey(event, modifiers) {
|
|
115
|
+
return event.ctrlKey === modifiers.ctrl && event.altKey === modifiers.alt && event.shiftKey === modifiers.shift && event.metaKey === modifiers.meta;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Hook for detecting push-to-talk hotkey press/release.
|
|
119
|
+
*
|
|
120
|
+
* @param hotkey - Hotkey string like "ctrl+alt" or "ctrl+shift"
|
|
121
|
+
* @param onPress - Called when hotkey is pressed
|
|
122
|
+
* @param onRelease - Called when hotkey is released
|
|
123
|
+
* @param enabled - Whether the hotkey listener is active (default: true)
|
|
124
|
+
*/
|
|
125
|
+
function useHotkey(hotkey, onPress, onRelease, enabled = true) {
|
|
126
|
+
const isPressedRef = useRef(false);
|
|
127
|
+
const modifiersRef = useRef(parseHotkey(hotkey));
|
|
128
|
+
const onPressRef = useRef(onPress);
|
|
129
|
+
const onReleaseRef = useRef(onRelease);
|
|
130
|
+
onPressRef.current = onPress;
|
|
131
|
+
onReleaseRef.current = onRelease;
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
modifiersRef.current = parseHotkey(hotkey);
|
|
134
|
+
}, [hotkey]);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!enabled) {
|
|
137
|
+
if (isPressedRef.current) {
|
|
138
|
+
isPressedRef.current = false;
|
|
139
|
+
onReleaseRef.current();
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
function handleKeyDown(event) {
|
|
144
|
+
if (matchesHotkey(event, modifiersRef.current) && !isPressedRef.current) {
|
|
145
|
+
isPressedRef.current = true;
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
onPressRef.current();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function handleKeyUp(event) {
|
|
151
|
+
if (isPressedRef.current && !matchesHotkey(event, modifiersRef.current)) {
|
|
152
|
+
isPressedRef.current = false;
|
|
153
|
+
onReleaseRef.current();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function handleBlur() {
|
|
157
|
+
if (isPressedRef.current) {
|
|
158
|
+
isPressedRef.current = false;
|
|
159
|
+
onReleaseRef.current();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
163
|
+
window.addEventListener("keyup", handleKeyUp);
|
|
164
|
+
window.addEventListener("blur", handleBlur);
|
|
165
|
+
return () => {
|
|
166
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
167
|
+
window.removeEventListener("keyup", handleKeyUp);
|
|
168
|
+
window.removeEventListener("blur", handleBlur);
|
|
169
|
+
};
|
|
170
|
+
}, [enabled]);
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
98
173
|
//#region src/react/components/Cursor.tsx
|
|
99
174
|
const BASE_ROTATION = -Math.PI / 6;
|
|
100
175
|
/**
|
|
@@ -107,7 +182,10 @@ function DefaultCursor({ state, rotation, scale }) {
|
|
|
107
182
|
height: "32",
|
|
108
183
|
viewBox: "0 0 32 32",
|
|
109
184
|
className: `cursor-buddy-cursor ${`cursor-buddy-cursor--${state}`}`,
|
|
110
|
-
style: {
|
|
185
|
+
style: {
|
|
186
|
+
transform: `rotate(${BASE_ROTATION + rotation}rad) scale(${scale})`,
|
|
187
|
+
transformOrigin: "16px 4px"
|
|
188
|
+
},
|
|
111
189
|
children: /* @__PURE__ */ jsx("polygon", { points: "16,4 28,28 16,22 4,28" })
|
|
112
190
|
});
|
|
113
191
|
}
|
|
@@ -135,21 +213,33 @@ function DefaultSpeechBubble({ text, isVisible, onClick }) {
|
|
|
135
213
|
}
|
|
136
214
|
//#endregion
|
|
137
215
|
//#region src/react/components/Waveform.tsx
|
|
138
|
-
const
|
|
216
|
+
const EMPTY_BARS = Array.from({ length: 12 }, () => 0);
|
|
139
217
|
/**
|
|
140
218
|
* Default waveform component.
|
|
141
219
|
* Shows audio level visualization during recording.
|
|
142
220
|
*/
|
|
143
221
|
function DefaultWaveform({ audioLevel, isListening }) {
|
|
222
|
+
const [bars, setBars] = useState(EMPTY_BARS);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!isListening) {
|
|
225
|
+
setBars(EMPTY_BARS);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setBars((previousBars) => {
|
|
229
|
+
const nextBars = previousBars.slice(1);
|
|
230
|
+
nextBars.push(audioLevel);
|
|
231
|
+
return nextBars;
|
|
232
|
+
});
|
|
233
|
+
}, [audioLevel, isListening]);
|
|
144
234
|
if (!isListening) return null;
|
|
145
235
|
return /* @__PURE__ */ jsx("div", {
|
|
146
236
|
className: "cursor-buddy-waveform",
|
|
147
|
-
children:
|
|
237
|
+
children: bars.map((level) => Math.pow(level, .65)).map((level, i) => {
|
|
148
238
|
const baseHeight = 4;
|
|
149
|
-
const variance =
|
|
239
|
+
const variance = .75 + (i + 1) % 3 * .12;
|
|
150
240
|
return /* @__PURE__ */ jsx("div", {
|
|
151
241
|
className: "cursor-buddy-waveform-bar",
|
|
152
|
-
style: { height: `${baseHeight +
|
|
242
|
+
style: { height: `${baseHeight + level * 20 * variance}px` }
|
|
153
243
|
}, i);
|
|
154
244
|
})
|
|
155
245
|
});
|
|
@@ -209,81 +299,6 @@ function Overlay({ cursor, speechBubble, waveform, container }) {
|
|
|
209
299
|
return createPortal(overlayContent, portalContainer);
|
|
210
300
|
}
|
|
211
301
|
//#endregion
|
|
212
|
-
//#region src/react/use-hotkey.ts
|
|
213
|
-
/**
|
|
214
|
-
* Parse a hotkey string like "ctrl+alt" into modifier flags
|
|
215
|
-
*/
|
|
216
|
-
function parseHotkey(hotkey) {
|
|
217
|
-
const parts = hotkey.toLowerCase().split("+");
|
|
218
|
-
return {
|
|
219
|
-
ctrl: parts.includes("ctrl") || parts.includes("control"),
|
|
220
|
-
alt: parts.includes("alt") || parts.includes("option"),
|
|
221
|
-
shift: parts.includes("shift"),
|
|
222
|
-
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command")
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Check if a keyboard event matches the required modifiers
|
|
227
|
-
*/
|
|
228
|
-
function matchesHotkey(event, modifiers) {
|
|
229
|
-
return event.ctrlKey === modifiers.ctrl && event.altKey === modifiers.alt && event.shiftKey === modifiers.shift && event.metaKey === modifiers.meta;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Hook for detecting push-to-talk hotkey press/release.
|
|
233
|
-
*
|
|
234
|
-
* @param hotkey - Hotkey string like "ctrl+alt" or "ctrl+shift"
|
|
235
|
-
* @param onPress - Called when hotkey is pressed
|
|
236
|
-
* @param onRelease - Called when hotkey is released
|
|
237
|
-
* @param enabled - Whether the hotkey listener is active (default: true)
|
|
238
|
-
*/
|
|
239
|
-
function useHotkey(hotkey, onPress, onRelease, enabled = true) {
|
|
240
|
-
const isPressedRef = useRef(false);
|
|
241
|
-
const modifiersRef = useRef(parseHotkey(hotkey));
|
|
242
|
-
const onPressRef = useRef(onPress);
|
|
243
|
-
const onReleaseRef = useRef(onRelease);
|
|
244
|
-
onPressRef.current = onPress;
|
|
245
|
-
onReleaseRef.current = onRelease;
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
modifiersRef.current = parseHotkey(hotkey);
|
|
248
|
-
}, [hotkey]);
|
|
249
|
-
useEffect(() => {
|
|
250
|
-
if (!enabled) {
|
|
251
|
-
if (isPressedRef.current) {
|
|
252
|
-
isPressedRef.current = false;
|
|
253
|
-
onReleaseRef.current();
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
function handleKeyDown(event) {
|
|
258
|
-
if (matchesHotkey(event, modifiersRef.current) && !isPressedRef.current) {
|
|
259
|
-
isPressedRef.current = true;
|
|
260
|
-
event.preventDefault();
|
|
261
|
-
onPressRef.current();
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function handleKeyUp(event) {
|
|
265
|
-
if (isPressedRef.current && !matchesHotkey(event, modifiersRef.current)) {
|
|
266
|
-
isPressedRef.current = false;
|
|
267
|
-
onReleaseRef.current();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function handleBlur() {
|
|
271
|
-
if (isPressedRef.current) {
|
|
272
|
-
isPressedRef.current = false;
|
|
273
|
-
onReleaseRef.current();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
277
|
-
window.addEventListener("keyup", handleKeyUp);
|
|
278
|
-
window.addEventListener("blur", handleBlur);
|
|
279
|
-
return () => {
|
|
280
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
281
|
-
window.removeEventListener("keyup", handleKeyUp);
|
|
282
|
-
window.removeEventListener("blur", handleBlur);
|
|
283
|
-
};
|
|
284
|
-
}, [enabled]);
|
|
285
|
-
}
|
|
286
|
-
//#endregion
|
|
287
302
|
//#region src/react/components/CursorBuddy.tsx
|
|
288
303
|
/**
|
|
289
304
|
* Internal component that sets up hotkey handling
|