uilint-vision 0.2.136 → 0.2.137

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.
@@ -628,4 +628,4 @@ export {
628
628
  visionPlugin,
629
629
  plugin_default
630
630
  };
631
- //# sourceMappingURL=chunk-CM75UYJ5.js.map
631
+ //# sourceMappingURL=chunk-GFLTUBO7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin/index.ts","../src/types.ts","../src/plugin/state.ts","../src/plugin/actions.ts","../src/plugin/commands.ts","../src/plugin/toolbar.ts","../src/plugin/panels.ts","../src/plugin/rules.ts","../src/plugin/messages.ts"],"sourcesContent":["/**\n * Vision Plugin Definition\n *\n * Complete plugin export - NO REACT.\n * This is the main plugin definition that gets registered with pluginRegistry.\n */\n\nimport type { PluginWithHandlers, PluginIssue, IssueContribution } from \"uilint-core\";\nimport { pluginRegistry } from \"uilint-core\";\n\nimport { visionStateDefinition, type VisionState } from \"./state.js\";\nimport { visionActionHandlers } from \"./actions.js\";\nimport { visionCommands } from \"./commands.js\";\nimport { visionToolbarGroups } from \"./toolbar.js\";\nimport { visionPanelDefinitions } from \"./panels.js\";\nimport { visionRuleDefinitions } from \"./rules.js\";\nimport { visionMessageHandlers } from \"./messages.js\";\n\n/**\n * Vision plugin definition\n *\n * Contains everything needed for the vision feature EXCEPT React components.\n * The host (uilint-react) imports this and renders the appropriate UI.\n */\nexport const visionPlugin: PluginWithHandlers<VisionState> = {\n // === Metadata ===\n id: \"vision\",\n name: \"Vision Analysis\",\n version: \"1.0.0\",\n description: \"AI-powered visual UI consistency checking\",\n icon: \"eye\",\n\n // === State ===\n state: visionStateDefinition,\n actions: visionActionHandlers,\n\n // === UI Contributions (Declarative) ===\n commands: visionCommands,\n toolbarGroups: visionToolbarGroups,\n panels: visionPanelDefinitions,\n\n // === Rules ===\n rules: visionRuleDefinitions,\n handlesRuleCategories: [\"vision\"],\n\n // === WebSocket ===\n messageHandlers: visionMessageHandlers,\n\n // === Issue Aggregation ===\n getIssues: (state: VisionState): IssueContribution => {\n const issues = new Map<string, PluginIssue[]>();\n\n for (const [route, visionIssues] of state.visionIssuesCache) {\n for (const issue of visionIssues) {\n if (issue.dataLoc) {\n const existing = issues.get(issue.dataLoc) || [];\n issues.set(issue.dataLoc, [\n ...existing,\n {\n id: `vision:${route}:${issue.elementText}:${Date.now()}`,\n message: issue.message,\n severity: issue.severity,\n ruleId: \"semantic-vision\",\n category: issue.category,\n data: {\n route,\n elementText: issue.elementText,\n suggestion: issue.suggestion,\n },\n },\n ]);\n }\n }\n }\n\n return { pluginId: \"vision\", issues };\n },\n\n // === Browser Actions ===\n browserActions: [\"capture-screenshot\", \"collect-manifest\"],\n\n // === Lifecycle ===\n onLoad: (ctx) => {\n // Check vision availability on load if connected\n if (ctx.websocket.isConnected) {\n ctx.websocket.send({ type: \"vision:check\" });\n }\n },\n};\n\n// Auto-register with plugin registry on import\npluginRegistry.register(visionPlugin);\n\n// Export types for host apps\nexport type { VisionState } from \"./state.js\";\n\nexport default visionPlugin;\n","/**\n * Vision Plugin Types\n *\n * All types for vision-based UI consistency analysis.\n * Consolidated from uilint-core and uilint-react.\n */\n\n// =============================================================================\n// CORE TYPES\n// =============================================================================\n\n/**\n * Vision issue category\n */\nexport type VisionIssueCategory =\n | \"spacing\"\n | \"alignment\"\n | \"color\"\n | \"typography\"\n | \"layout\"\n | \"contrast\"\n | \"visual-hierarchy\"\n | \"other\";\n\n/**\n * Vision issue severity\n */\nexport type VisionIssueSeverity = \"error\" | \"warning\" | \"info\";\n\n/**\n * Vision analysis issue from the LLM\n */\nexport interface VisionIssue {\n /** Text of the element this issue refers to */\n elementText: string;\n /** Issue description */\n message: string;\n /** Issue category */\n category: VisionIssueCategory;\n /** Severity level */\n severity: VisionIssueSeverity;\n /** Matched dataLoc from manifest (filled in after text matching) */\n dataLoc?: string;\n /** Matched element ID (filled in after text matching) */\n elementId?: string;\n /** Suggested fix (optional) */\n suggestion?: string;\n}\n\n/**\n * Element manifest entry for vision analysis.\n * Maps visible elements to their source locations.\n */\nexport interface ElementManifest {\n /** Unique ID (data-loc if present, otherwise generated) */\n id: string;\n /** Visible text content (truncated to 100 chars) */\n text: string;\n /** data-loc value: \"path:line:column\" */\n dataLoc: string;\n /** Bounding rectangle */\n rect: { x: number; y: number; width: number; height: number };\n /** HTML tag name */\n tagName: string;\n /** Inferred semantic role (button, heading, link, etc.) */\n role?: string;\n /** Total instances with same dataLoc (if deduplicated) */\n instanceCount?: number;\n}\n\n// =============================================================================\n// CAPTURE TYPES\n// =============================================================================\n\n/**\n * Region bounds for partial screenshot capture\n */\nexport interface CaptureRegion {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Capture mode for vision analysis\n */\nexport type CaptureMode = \"full\" | \"region\";\n\n/**\n * Screenshot capture entry for the gallery\n */\nexport interface ScreenshotCapture {\n /** Unique ID for this capture */\n id: string;\n /** Route where the capture was taken */\n route: string;\n /** Base64 data URL of the screenshot (for in-memory captures) */\n dataUrl?: string;\n /** Filename for persisted screenshots (used to fetch from API) */\n filename?: string;\n /** Unix timestamp when captured */\n timestamp: number;\n /** Type of capture */\n type: CaptureMode;\n /** Region bounds if type is 'region' */\n region?: CaptureRegion;\n /** Whether this is a persisted screenshot loaded from disk */\n persisted?: boolean;\n /** Vision issues specific to this capture */\n issues?: VisionIssue[];\n}\n\n// =============================================================================\n// ANALYSIS TYPES\n// =============================================================================\n\n/**\n * Vision analysis result from the analyzer\n */\nexport interface VisionAnalysisResult {\n /** Route/path that was analyzed */\n route: string;\n /** Timestamp of capture */\n timestamp: number;\n /** Screenshot as base64 data URL */\n screenshotDataUrl?: string;\n /** Element manifest */\n manifest: ElementManifest[];\n /** Issues found by vision analysis */\n issues: VisionIssue[];\n /** Analysis duration in ms */\n analysisTime: number;\n /** Error message if analysis failed */\n error?: string;\n /** Full prompt sent to the model (for debugging) */\n prompt?: string;\n /** Raw LLM response (for debugging) */\n rawResponse?: string;\n}\n\n// =============================================================================\n// SETTINGS & STATE TYPES\n// =============================================================================\n\n/**\n * Auto-scan settings for Vision analysis.\n * Persisted to localStorage.\n */\nexport interface VisionAutoScanSettings {\n /** Auto-capture and analyze on route change */\n onRouteChange: boolean;\n /** Auto-capture and analyze on initial page load */\n onInitialLoad: boolean;\n}\n\n/**\n * Default vision auto-scan settings\n */\nexport const DEFAULT_VISION_AUTO_SCAN_SETTINGS: VisionAutoScanSettings = {\n onRouteChange: false,\n onInitialLoad: false,\n};\n\n/**\n * Vision pipeline stage (for error tracking)\n */\nexport type VisionStage = \"capture\" | \"manifest\" | \"ws\" | \"vision\";\n\n/**\n * Vision error information with stage context\n */\nexport interface VisionErrorInfo {\n /** The stage where the error occurred */\n stage: VisionStage;\n /** Human-readable error message */\n message: string;\n /** Route that was being analyzed */\n route: string;\n /** Timestamp of the error */\n timestamp: number;\n}\n\n// =============================================================================\n// PERSISTENCE TYPES\n// =============================================================================\n\n/**\n * Persisted screenshot metadata from the API\n */\nexport interface PersistedScreenshotMetadata {\n filename: string;\n timestamp: number;\n screenshotFile: string;\n route: string | null;\n issues: VisionIssue[] | null;\n manifest: ElementManifest[] | null;\n analysisResult: {\n route: string;\n timestamp: number;\n issues: VisionIssue[];\n analysisTime: number;\n error?: string;\n } | null;\n}\n\n/**\n * API response for listing screenshots\n */\nexport interface ScreenshotListResponse {\n screenshots: Array<{\n filename: string;\n metadata: PersistedScreenshotMetadata | null;\n }>;\n projectRoot: string;\n screenshotsDir: string;\n}\n\n// =============================================================================\n// WEBSOCKET MESSAGE TYPES\n// =============================================================================\n\n/**\n * Client -> Server: Request vision analysis\n */\nexport interface VisionAnalyzeMessage {\n type: \"vision:analyze\";\n route: string;\n timestamp: number;\n screenshot: string;\n manifest: ElementManifest[];\n requestId?: string;\n}\n\n/**\n * Server -> Client: Vision analysis result\n */\nexport interface VisionResultMessage {\n type: \"vision:result\";\n route: string;\n issues: VisionIssue[];\n analysisTime: number;\n error?: string;\n requestId?: string;\n}\n\n/**\n * Server -> Client: Vision analysis progress\n */\nexport interface VisionProgressMessage {\n type: \"vision:progress\";\n route: string;\n phase: string;\n requestId?: string;\n}\n\n/**\n * Client -> Server: Check vision availability\n */\nexport interface VisionCheckMessage {\n type: \"vision:check\";\n requestId?: string;\n}\n\n/**\n * Server -> Client: Vision availability status\n */\nexport interface VisionStatusMessage {\n type: \"vision:status\";\n available: boolean;\n model?: string;\n requestId?: string;\n}\n\n/**\n * Union of all vision WebSocket messages\n */\nexport type VisionMessage =\n | VisionAnalyzeMessage\n | VisionResultMessage\n | VisionProgressMessage\n | VisionCheckMessage\n | VisionStatusMessage;\n","/**\n * Vision Plugin State\n *\n * State shape and initial state for the vision plugin.\n * No React - pure TypeScript.\n */\n\nimport type { StateDefinition } from \"uilint-core\";\nimport type {\n ScreenshotCapture,\n VisionIssue,\n VisionAutoScanSettings,\n VisionErrorInfo,\n CaptureMode,\n CaptureRegion,\n} from \"../types.js\";\nimport { DEFAULT_VISION_AUTO_SCAN_SETTINGS } from \"../types.js\";\n\n/**\n * Vision plugin state shape\n */\nexport interface VisionState {\n // === Availability ===\n /** Whether vision analysis is available (Ollama running with vision model) */\n visionAvailable: boolean;\n /** The vision model being used */\n visionModel: string | null;\n\n // === Analysis State ===\n /** Whether vision analysis is in progress */\n visionAnalyzing: boolean;\n /** Current phase of analysis (capture, manifest, analyzing, etc.) */\n visionProgressPhase: string | null;\n\n // === Capture State ===\n /** Current capture mode */\n captureMode: CaptureMode;\n /** Whether region selection is active */\n regionSelectionActive: boolean;\n /** Selected region bounds (if in region mode) */\n selectedRegion: CaptureRegion | null;\n\n // === Results ===\n /** Screenshot history keyed by ID */\n screenshotHistory: Map<string, ScreenshotCapture>;\n /** Vision issues keyed by route */\n visionIssuesCache: Map<string, VisionIssue[]>;\n\n // === Error State ===\n /** Last error that occurred */\n lastError: VisionErrorInfo | null;\n\n // === Settings ===\n /** Auto-scan settings */\n autoScanSettings: VisionAutoScanSettings;\n}\n\n/**\n * Initial state for the vision plugin\n */\nexport const visionInitialState: VisionState = {\n // Availability\n visionAvailable: false,\n visionModel: null,\n\n // Analysis state\n visionAnalyzing: false,\n visionProgressPhase: null,\n\n // Capture state\n captureMode: \"full\",\n regionSelectionActive: false,\n selectedRegion: null,\n\n // Results\n screenshotHistory: new Map(),\n visionIssuesCache: new Map(),\n\n // Error state\n lastError: null,\n\n // Settings\n autoScanSettings: DEFAULT_VISION_AUTO_SCAN_SETTINGS,\n};\n\n/**\n * State definition for the plugin system\n */\nexport const visionStateDefinition: StateDefinition<VisionState> = {\n initialState: visionInitialState,\n\n computed: {\n /** Whether there are any screenshots */\n hasScreenshots: (state) => state.screenshotHistory.size > 0,\n\n /** Total number of issues across all routes */\n totalIssues: (state) => {\n let count = 0;\n for (const issues of state.visionIssuesCache.values()) {\n count += issues.length;\n }\n return count;\n },\n\n /** Whether currently capturing */\n isCapturing: (state) =>\n state.visionAnalyzing && state.visionProgressPhase === \"capture\",\n\n /** Whether currently analyzing (after capture) */\n isAnalyzing: (state) =>\n state.visionAnalyzing && state.visionProgressPhase === \"analyzing\",\n\n /** All screenshots as an array (sorted by timestamp, newest first) */\n screenshotList: (state) =>\n Array.from(state.screenshotHistory.values()).sort(\n (a, b) => b.timestamp - a.timestamp\n ),\n },\n\n persist: {\n key: \"uilint-vision\",\n include: [\"autoScanSettings\"],\n },\n};\n","/**\n * Vision Plugin Action Handlers\n *\n * Plain functions that handle plugin actions.\n * No React - uses PluginContext for state management.\n */\n\nimport type { ActionHandlers, PluginContext } from \"uilint-core\";\nimport type { VisionState } from \"./state.js\";\nimport type {\n VisionIssue,\n ScreenshotCapture,\n CaptureRegion,\n VisionAutoScanSettings,\n} from \"../types.js\";\n\n// Helper type for cleaner action handler definitions\ntype Handler<TPayload = void> = (\n ctx: PluginContext<VisionState>,\n payload: TPayload\n) => void | Promise<void>;\n\n// Cast helper for proper typing - wraps typed handlers to satisfy ActionHandlers\nconst h = <TPayload = void>(fn: Handler<TPayload>): Handler<unknown> =>\n fn as Handler<unknown>;\n\n/**\n * Action handlers for the vision plugin\n */\nexport const visionActionHandlers: ActionHandlers<VisionState> = {\n /**\n * Set vision availability status\n */\n \"set-vision-available\": h<{ available: boolean; model?: string }>((ctx, payload) => {\n ctx.setState({\n visionAvailable: payload.available,\n visionModel: payload.model ?? null,\n });\n }),\n\n /**\n * Trigger full page capture\n */\n \"capture-full-page\": h(async (ctx) => {\n ctx.setState({\n captureMode: \"full\",\n regionSelectionActive: false,\n selectedRegion: null,\n });\n await ctx.dispatch(\"trigger-vision-analysis\");\n }),\n\n /**\n * Enter region selection mode\n */\n \"enter-region-selection\": h((ctx) => {\n ctx.setState({\n captureMode: \"region\",\n regionSelectionActive: true,\n selectedRegion: null,\n });\n }),\n\n /**\n * Exit region selection mode\n */\n \"exit-region-selection\": h((ctx) => {\n ctx.setState({\n regionSelectionActive: false,\n selectedRegion: null,\n });\n }),\n\n /**\n * Set selected region and trigger capture\n */\n \"set-selected-region\": h<{ region: CaptureRegion }>(async (ctx, payload) => {\n ctx.setState({\n selectedRegion: payload.region,\n regionSelectionActive: false,\n });\n await ctx.dispatch(\"trigger-vision-analysis\");\n }),\n\n /**\n * Trigger vision analysis\n * Requests browser actions for capture and manifest, then sends to server\n */\n \"trigger-vision-analysis\": h(async (ctx) => {\n const state = ctx.getState();\n\n if (!state.visionAvailable) {\n ctx.setState({\n lastError: {\n stage: \"vision\",\n message: \"Vision analysis not available. Check Ollama connection.\",\n route: ctx.getCurrentRoute(),\n timestamp: Date.now(),\n },\n });\n return;\n }\n\n ctx.setState({\n visionAnalyzing: true,\n visionProgressPhase: \"capture\",\n lastError: null,\n });\n\n try {\n // Request screenshot capture from browser\n const captureResult = await ctx.requestBrowserAction(\"capture-screenshot\", {\n mode: state.captureMode,\n region: state.selectedRegion,\n });\n\n if (!captureResult.success) {\n throw new Error(captureResult.error || \"Failed to capture screenshot\");\n }\n\n ctx.setState({ visionProgressPhase: \"manifest\" });\n\n // Request manifest collection from browser\n const manifestResult = await ctx.requestBrowserAction(\"collect-manifest\", {\n region: state.selectedRegion,\n });\n\n if (!manifestResult.success) {\n throw new Error(manifestResult.error || \"Failed to collect manifest\");\n }\n\n ctx.setState({ visionProgressPhase: \"analyzing\" });\n\n const route = ctx.getCurrentRoute();\n const timestamp = Date.now();\n\n // Add to screenshot history\n const capture: ScreenshotCapture = {\n id: `capture-${timestamp}`,\n route,\n dataUrl: captureResult.dataUrl as string,\n timestamp,\n type: state.captureMode,\n region: state.selectedRegion ?? undefined,\n };\n\n const newHistory = new Map(state.screenshotHistory);\n newHistory.set(capture.id, capture);\n ctx.setState({ screenshotHistory: newHistory });\n\n // Send to server for analysis\n ctx.websocket.send({\n type: \"vision:analyze\",\n route,\n timestamp,\n screenshot: captureResult.dataUrl,\n manifest: manifestResult.elements,\n });\n } catch (error) {\n ctx.setState({\n visionAnalyzing: false,\n visionProgressPhase: null,\n lastError: {\n stage: \"capture\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n route: ctx.getCurrentRoute(),\n timestamp: Date.now(),\n },\n });\n }\n }),\n\n /**\n * Handle vision analysis result from server\n */\n \"handle-vision-result\": h<{\n route: string;\n issues: VisionIssue[];\n analysisTime?: number;\n error?: string;\n }>((ctx, payload) => {\n const state = ctx.getState();\n\n if (payload.error) {\n ctx.setState({\n visionAnalyzing: false,\n visionProgressPhase: null,\n lastError: {\n stage: \"vision\",\n message: payload.error,\n route: payload.route,\n timestamp: Date.now(),\n },\n });\n return;\n }\n\n // Update issues cache\n const newCache = new Map(state.visionIssuesCache);\n newCache.set(payload.route, payload.issues);\n\n // Update the latest capture with issues\n const latestCapture = Array.from(state.screenshotHistory.values())\n .filter((c) => c.route === payload.route)\n .sort((a, b) => b.timestamp - a.timestamp)[0];\n\n if (latestCapture) {\n const newHistory = new Map(state.screenshotHistory);\n newHistory.set(latestCapture.id, { ...latestCapture, issues: payload.issues });\n ctx.setState({ screenshotHistory: newHistory });\n }\n\n ctx.setState({\n visionAnalyzing: false,\n visionProgressPhase: null,\n visionIssuesCache: newCache,\n lastError: null,\n });\n }),\n\n /**\n * Handle vision progress update\n */\n \"handle-vision-progress\": h<{ phase: string }>((ctx, payload) => {\n ctx.setState({ visionProgressPhase: payload.phase });\n }),\n\n /**\n * Clear all screenshots and cached issues\n */\n \"clear-screenshots\": h((ctx) => {\n ctx.setState({\n screenshotHistory: new Map(),\n visionIssuesCache: new Map(),\n });\n }),\n\n /**\n * Delete a specific screenshot\n */\n \"delete-screenshot\": h<{ id: string }>((ctx, payload) => {\n const state = ctx.getState();\n const newHistory = new Map(state.screenshotHistory);\n newHistory.delete(payload.id);\n ctx.setState({ screenshotHistory: newHistory });\n }),\n\n /**\n * Update auto-scan settings\n */\n \"update-auto-scan-settings\": h<Partial<VisionAutoScanSettings>>((ctx, payload) => {\n const state = ctx.getState();\n ctx.setState({\n autoScanSettings: { ...state.autoScanSettings, ...payload },\n });\n }),\n\n /**\n * Focus an issue in the heatmap\n */\n \"focus-heatmap\": h<{ dataLoc: string }>((ctx, payload) => {\n ctx.setHeatmapFilter([payload.dataLoc], \"Vision Issue\");\n }),\n\n /**\n * Clear heatmap filter\n */\n \"clear-heatmap-filter\": h((ctx) => {\n ctx.clearHeatmapFilter();\n }),\n\n /**\n * Open file in editor\n */\n \"open-editor\": h<{ dataLoc: string }>((ctx, payload) => {\n ctx.openInEditor(payload.dataLoc);\n }),\n\n /**\n * Select a capture to view in inspector\n */\n \"select-capture\": h<{ captureId: string }>((ctx, payload) => {\n const state = ctx.getState();\n const capture = state.screenshotHistory.get(payload.captureId);\n if (capture) {\n ctx.openInspector(\"vision-gallery\", { capture });\n }\n }),\n};\n","/**\n * Vision Plugin Commands\n *\n * Command palette commands for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { CommandDefinition } from \"uilint-core\";\n\n/**\n * Vision plugin commands\n */\nexport const visionCommands: CommandDefinition[] = [\n {\n id: \"vision:capture-full-page\",\n title: \"Capture Full Page\",\n keywords: [\"vision\", \"screenshot\", \"capture\", \"full\", \"page\", \"analyze\"],\n category: \"Vision\",\n subtitle: \"Capture and analyze the entire visible page\",\n icon: \"camera\",\n shortcut: \"Cmd+Shift+C\",\n action: { type: \"capture-full-page\" },\n isAvailable: { binding: \"visionAvailable\" },\n },\n {\n id: \"vision:capture-region\",\n title: \"Capture Region\",\n keywords: [\"vision\", \"screenshot\", \"capture\", \"region\", \"area\", \"select\"],\n category: \"Vision\",\n subtitle: \"Select a region of the page to capture and analyze\",\n icon: \"crop\",\n shortcut: \"Cmd+Shift+R\",\n action: { type: \"enter-region-selection\" },\n isAvailable: { binding: \"visionAvailable\" },\n },\n {\n id: \"vision:clear-screenshots\",\n title: \"Clear Screenshots\",\n keywords: [\"vision\", \"clear\", \"delete\", \"screenshots\", \"history\"],\n category: \"Vision\",\n subtitle: \"Clear all captured screenshots and cached results\",\n icon: \"trash\",\n action: { type: \"clear-screenshots\" },\n isAvailable: { expression: \"screenshotHistory.size > 0\" },\n },\n {\n id: \"vision:toggle-auto-scan-route\",\n title: \"Toggle Auto-Scan on Route Change\",\n keywords: [\"vision\", \"auto\", \"scan\", \"route\", \"change\", \"automatic\"],\n category: \"Vision\",\n subtitle: \"Automatically capture and analyze on route change\",\n icon: \"refresh\",\n action: {\n type: \"update-auto-scan-settings\",\n payloadBindings: { onRouteChange: \"!autoScanSettings.onRouteChange\" },\n },\n isAvailable: { binding: \"visionAvailable\" },\n },\n];\n","/**\n * Vision Plugin Toolbar\n *\n * Toolbar action group definitions for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { ToolbarGroupDefinition } from \"uilint-core\";\n\n/**\n * Vision capture toolbar group\n */\nexport const visionToolbarGroup: ToolbarGroupDefinition = {\n id: \"vision:capture\",\n icon: \"camera\",\n tooltip: \"Vision Capture\",\n priority: 100,\n isVisible: { binding: \"visionAvailable\" },\n\n actions: [\n {\n id: \"vision:capture-full\",\n icon: \"camera\",\n tooltip: \"Capture Full Page\",\n action: { type: \"capture-full-page\" },\n },\n {\n id: \"vision:capture-region\",\n icon: \"crop\",\n tooltip: \"Capture Region\",\n action: { type: \"enter-region-selection\" },\n },\n ],\n};\n\n/**\n * All toolbar groups for the vision plugin\n */\nexport const visionToolbarGroups: ToolbarGroupDefinition[] = [visionToolbarGroup];\n","/**\n * Vision Plugin Panels\n *\n * Inspector panel definitions for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { PanelDefinition } from \"uilint-core\";\n\n/**\n * Vision issue inspector panel\n */\nexport const visionIssuePanelDefinition: PanelDefinition = {\n id: \"vision-issue\",\n title: { binding: \"issue.category\" },\n priority: 10,\n\n empty: {\n when: { expression: \"!issue\" },\n message: \"No vision issue selected.\",\n icon: \"eye\",\n },\n\n layout: [\n // Header with message\n {\n type: \"header\",\n icon: \"eye\",\n text: { binding: \"issue.message\" },\n sticky: true,\n },\n\n // Severity badge\n {\n type: \"badge\",\n variant: \"severity\",\n value: { binding: \"issue.severity\" },\n centered: false,\n },\n\n // Category badge\n {\n type: \"badge\",\n variant: \"category\",\n value: { binding: \"issue.category\" },\n centered: false,\n },\n\n // Element text (if available)\n {\n type: \"conditional\",\n condition: { binding: \"issue.elementText\" },\n then: [\n {\n type: \"text\",\n content: { binding: \"issue.elementText\" },\n variant: \"caption\",\n },\n ],\n },\n\n // Suggestion (if available)\n {\n type: \"conditional\",\n condition: { binding: \"issue.suggestion\" },\n then: [\n { type: \"divider\", spacing: \"small\" },\n {\n type: \"header\",\n icon: \"sparkles\",\n text: \"Suggestion\",\n },\n {\n type: \"text\",\n content: { binding: \"issue.suggestion\" },\n variant: \"body\",\n },\n ],\n },\n\n { type: \"divider\", spacing: \"medium\" },\n\n // Actions\n {\n type: \"actions\",\n direction: \"column\",\n actions: [\n {\n id: \"focus-heatmap\",\n label: \"Focus in Heatmap\",\n icon: \"filter\",\n variant: \"secondary\",\n action: {\n type: \"focus-heatmap\",\n payloadBindings: { dataLoc: \"issue.dataLoc\" },\n },\n visible: { binding: \"issue.dataLoc\" },\n },\n {\n id: \"open-editor\",\n label: \"Open in Editor\",\n icon: \"external-link\",\n variant: \"ghost\",\n action: {\n type: \"open-editor\",\n payloadBindings: { dataLoc: \"issue.dataLoc\" },\n },\n visible: { binding: \"issue.dataLoc\" },\n },\n ],\n },\n ],\n};\n\n/**\n * Screenshot gallery panel\n */\nexport const screenshotGalleryPanelDefinition: PanelDefinition = {\n id: \"vision-gallery\",\n title: \"Screenshots\",\n priority: 5,\n\n empty: {\n when: { expression: \"screenshots.length === 0\" },\n message: \"No screenshots captured yet.\",\n submessage: \"Use the capture button to take a screenshot.\",\n icon: \"camera\",\n },\n\n layout: [\n {\n type: \"list\",\n items: { binding: \"screenshots\" },\n itemLayout: [\n {\n type: \"card\",\n thumbnail: { binding: \"item.dataUrl\" },\n title: { binding: \"item.route\" },\n subtitle: { binding: \"item.timestamp\" },\n badge: {\n variant: \"count\",\n value: { binding: \"item.issues.length\" },\n label: \"issues\",\n },\n onClick: {\n type: \"select-capture\",\n payloadBindings: { captureId: \"item.id\" },\n },\n },\n ],\n },\n ],\n};\n\n/**\n * All vision panel definitions\n */\nexport const visionPanelDefinitions: PanelDefinition[] = [\n visionIssuePanelDefinition,\n screenshotGalleryPanelDefinition,\n];\n","/**\n * Vision Plugin Rules\n *\n * Rule definitions for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { RuleDefinition } from \"uilint-core\";\n\n/**\n * semantic-vision rule definition\n */\nexport const semanticVisionRuleDefinition: RuleDefinition = {\n id: \"semantic-vision\",\n name: \"Vision Analysis\",\n description: \"AI-powered visual UI consistency checking\",\n category: \"vision\",\n icon: \"eye\",\n defaultSeverity: \"warn\",\n defaultEnabled: false,\n heatmapColor: \"#8b5cf6\", // Purple\n customInspectorPanel: \"vision-issue\",\n requirements: [\n {\n type: \"ollama\",\n description: \"Requires Ollama with a vision model\",\n setupHint: \"Run: ollama pull qwen3-vl:8b-instruct\",\n },\n {\n type: \"connection\",\n description: \"Requires connection to uilint server\",\n setupHint: \"Run: uilint serve\",\n },\n ],\n docs: `\n## Vision Analysis Rule\n\nThis rule analyzes screenshots of your UI for visual consistency issues using AI vision models.\n\n### Categories of Issues\n\n- **Spacing**: Inconsistent margins, padding, or gaps\n- **Alignment**: Elements not properly aligned\n- **Color**: Color inconsistencies or accessibility issues\n- **Typography**: Font size, weight, or style inconsistencies\n- **Layout**: Layout problems or broken layouts\n- **Contrast**: Insufficient contrast for accessibility\n- **Visual Hierarchy**: Issues with visual importance/hierarchy\n\n### Requirements\n\n1. Ollama must be running with a vision model (qwen3-vl recommended)\n2. The UILint server must be connected\n\n### Usage\n\nCapture screenshots using the Vision toolbar or command palette, and issues will be reported automatically.\n `.trim(),\n};\n\n/**\n * All vision rule definitions\n */\nexport const visionRuleDefinitions: RuleDefinition[] = [semanticVisionRuleDefinition];\n","/**\n * Vision Plugin Message Handlers\n *\n * WebSocket message handlers for the vision plugin.\n * Plain functions - no React.\n */\n\nimport type { MessageHandlers, PluginContext } from \"uilint-core\";\nimport type { VisionState } from \"./state.js\";\nimport type {\n VisionStatusMessage,\n VisionProgressMessage,\n VisionResultMessage,\n} from \"../types.js\";\n\n/**\n * Message handlers for the vision plugin\n */\nexport const visionMessageHandlers: MessageHandlers<VisionState> = {\n /**\n * Handle vision availability status\n */\n \"vision:status\": (\n ctx: PluginContext<VisionState>,\n message: unknown\n ) => {\n const msg = message as VisionStatusMessage;\n ctx.dispatch(\"set-vision-available\", {\n available: msg.available,\n model: msg.model,\n });\n },\n\n /**\n * Handle vision analysis progress\n */\n \"vision:progress\": (\n ctx: PluginContext<VisionState>,\n message: unknown\n ) => {\n const msg = message as VisionProgressMessage;\n ctx.dispatch(\"handle-vision-progress\", { phase: msg.phase });\n },\n\n /**\n * Handle vision analysis result\n */\n \"vision:result\": (\n ctx: PluginContext<VisionState>,\n message: unknown\n ) => {\n const msg = message as VisionResultMessage;\n ctx.dispatch(\"handle-vision-result\", {\n route: msg.route,\n issues: msg.issues,\n analysisTime: msg.analysisTime,\n error: msg.error,\n });\n },\n};\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  DEFAULT_VISION_AUTO_SCAN_SETTINGS,
18
18
  visionPlugin
19
- } from "./chunk-CM75UYJ5.js";
19
+ } from "./chunk-GFLTUBO7.js";
20
20
  export {
21
21
  DEFAULT_VISION_AUTO_SCAN_SETTINGS,
22
22
  MANIFEST_SKIP_TAGS,
package/dist/node.d.ts CHANGED
@@ -4,6 +4,7 @@ export { MANIFEST_SKIP_TAGS, MANIFEST_TEXT_MAX_LENGTH, VISION_DEFAULT_MODEL, VIS
4
4
  export { default as visionPlugin } from './plugin/index.js';
5
5
  export { buildVisionAnalysisPayload, captureRegion, captureScreenshot, captureRegion as captureScreenshotRegion, collectElementManifest, generateTimestamp, getCurrentRoute, matchIssuesToManifest } from './browser/index.js';
6
6
  import { OllamaClientOptions, StreamProgressCallback, LLMInstrumentationCallbacks } from 'uilint-core';
7
+ import { StreamProgressCallback as StreamProgressCallback$1 } from 'uilint-core/node';
7
8
 
8
9
  /**
9
10
  * Vision analyzer for Ollama vision LLM analysis
@@ -117,4 +118,111 @@ declare class VisionAnalyzer {
117
118
  */
118
119
  declare function getVisionAnalyzer(options?: VisionAnalyzerOptions): VisionAnalyzer;
119
120
 
120
- export { type AnalyzerResult, ElementManifest, UILINT_DEFAULT_VISION_MODEL, type VisionAnalysisOptions, VisionAnalyzer, type VisionAnalyzerOptions, VisionIssue, getVisionAnalyzer };
121
+ /**
122
+ * Vision analysis utilities for running vision analysis with Ollama.
123
+ *
124
+ * These utilities provide high-level orchestration for vision analysis,
125
+ * including style guide resolution, Ollama readiness checks, and debug dump generation.
126
+ */
127
+
128
+ /**
129
+ * Path resolver function type.
130
+ * Resolves a path specifier (which may include workspace-relative "@" prefixes or other formats)
131
+ * into an absolute filesystem path.
132
+ */
133
+ type PathResolver = (spec: string, cwd: string) => string;
134
+ type ResolveVisionStyleGuideArgs = {
135
+ /** Project root / cwd used to resolve relative path specifiers */
136
+ projectPath: string;
137
+ /** Path to style guide file OR project directory */
138
+ styleguide?: string;
139
+ /** A starting point for upward search (directory). Defaults to projectPath. */
140
+ startDir?: string;
141
+ /** Optional custom path resolver (for handling workspace-relative paths like "@apps/...") */
142
+ pathResolver?: PathResolver;
143
+ };
144
+ type ResolveVisionStyleGuideResult = {
145
+ styleGuide: string | null;
146
+ styleguideLocation: string | null;
147
+ };
148
+ declare function resolveVisionStyleGuide(args: ResolveVisionStyleGuideArgs): Promise<ResolveVisionStyleGuideResult>;
149
+ type RunVisionAnalysisArgs = {
150
+ /** base64 image data (NO data: prefix) */
151
+ imageBase64: string;
152
+ manifest: ElementManifest[];
153
+ /** Used to resolve styleguide arg and as default search root */
154
+ projectPath: string;
155
+ /** Path to style guide file OR directory; if omitted uses upward search */
156
+ styleguide?: string;
157
+ /** Directory for upward styleguide search; defaults to projectPath */
158
+ styleguideStartDir?: string;
159
+ /**
160
+ * Override resolved styleguide content (lets callers do logging/notes based on resolution once).
161
+ * If provided (even null), styleguide resolution is skipped.
162
+ */
163
+ styleGuide?: string | null;
164
+ /**
165
+ * Override resolved styleguide location (pairs with styleGuide override).
166
+ * If provided, this is returned in the result.
167
+ */
168
+ styleguideLocation?: string | null;
169
+ /** Ollama base URL (default: http://localhost:11434) */
170
+ baseUrl?: string;
171
+ /** Vision model override (default: UILINT_DEFAULT_VISION_MODEL) */
172
+ model?: string;
173
+ /**
174
+ * Optional analyzer instance (lets servers reuse a singleton and lets tests inject a mock).
175
+ * If omitted, a new `VisionAnalyzer` is created with baseUrl+model.
176
+ */
177
+ analyzer?: Pick<VisionAnalyzer, "analyzeScreenshot">;
178
+ /** Optional streaming progress callback passed into the analyzer */
179
+ onProgress?: StreamProgressCallback$1;
180
+ /** Optional coarse-grained phase callback (good for spinners / WS progress) */
181
+ onPhase?: (phase: string) => void;
182
+ /** When true, skip calling `ensureOllamaReady` (caller is responsible). */
183
+ skipEnsureOllama?: boolean;
184
+ /** Optional debug dump destination (file path or directory) */
185
+ debugDump?: string;
186
+ /** When true, include base64 image and full styleguide in debug dump */
187
+ debugDumpIncludeSensitive?: boolean;
188
+ /** Optional extra metadata to include in debug dumps */
189
+ debugDumpMetadata?: Record<string, unknown>;
190
+ /** Optional custom path resolver (for handling workspace-relative paths like "@apps/...") */
191
+ pathResolver?: PathResolver;
192
+ };
193
+ type RunVisionAnalysisResult = {
194
+ issues: VisionIssue[];
195
+ analysisTime: number;
196
+ prompt?: string;
197
+ rawResponse?: string;
198
+ styleguideLocation: string | null;
199
+ visionModel: string;
200
+ baseUrl: string;
201
+ };
202
+ declare function runVisionAnalysis(args: RunVisionAnalysisArgs): Promise<RunVisionAnalysisResult>;
203
+ type WriteVisionMarkdownReportArgs = {
204
+ /** Absolute path to the source image file */
205
+ imagePath: string;
206
+ /** Route label (optional; included in report) */
207
+ route?: string;
208
+ /** Unix ms timestamp (optional; included in report) */
209
+ timestamp?: number;
210
+ visionModel?: string;
211
+ baseUrl?: string;
212
+ analysisTimeMs?: number;
213
+ prompt?: string | null;
214
+ rawResponse?: string | null;
215
+ /** Extra JSON-ish metadata to include */
216
+ metadata?: Record<string, unknown>;
217
+ /**
218
+ * Optional output path; if omitted, writes alongside the image:
219
+ * `<basename>.vision.md`
220
+ */
221
+ outPath?: string;
222
+ };
223
+ declare function writeVisionMarkdownReport(args: WriteVisionMarkdownReportArgs): {
224
+ outPath: string;
225
+ content: string;
226
+ };
227
+
228
+ export { type AnalyzerResult, ElementManifest, type PathResolver, type ResolveVisionStyleGuideArgs, type ResolveVisionStyleGuideResult, type RunVisionAnalysisArgs, type RunVisionAnalysisResult, UILINT_DEFAULT_VISION_MODEL, type VisionAnalysisOptions, VisionAnalyzer, type VisionAnalyzerOptions, VisionIssue, type WriteVisionMarkdownReportArgs, getVisionAnalyzer, resolveVisionStyleGuide, runVisionAnalysis, writeVisionMarkdownReport };
package/dist/node.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  DEFAULT_VISION_AUTO_SCAN_SETTINGS,
18
18
  visionPlugin
19
- } from "./chunk-CM75UYJ5.js";
19
+ } from "./chunk-GFLTUBO7.js";
20
20
 
21
21
  // src/analyzer/vision-analyzer.ts
22
22
  import { Ollama } from "ollama";
@@ -415,6 +415,200 @@ function getVisionAnalyzer(options) {
415
415
  }
416
416
  return defaultAnalyzer;
417
417
  }
418
+
419
+ // src/vision-run.ts
420
+ import { dirname, join, parse, resolve } from "path";
421
+ import { existsSync, statSync, mkdirSync, writeFileSync } from "fs";
422
+ import {
423
+ ensureOllamaReady,
424
+ findStyleGuidePath,
425
+ findUILintStyleGuideUpwards,
426
+ readStyleGuide
427
+ } from "uilint-core/node";
428
+ var defaultPathResolver = (spec, cwd) => {
429
+ const raw = (spec ?? "").trim();
430
+ if (!raw) return resolve(cwd, spec);
431
+ return resolve(cwd, raw);
432
+ };
433
+ async function resolveVisionStyleGuide(args) {
434
+ const projectPath = args.projectPath;
435
+ const startDir = args.startDir ?? projectPath;
436
+ const resolvePath = args.pathResolver ?? defaultPathResolver;
437
+ if (args.styleguide) {
438
+ const styleguideArg = resolvePath(args.styleguide, projectPath);
439
+ if (existsSync(styleguideArg)) {
440
+ const stat = statSync(styleguideArg);
441
+ if (stat.isFile()) {
442
+ return {
443
+ styleguideLocation: styleguideArg,
444
+ styleGuide: await readStyleGuide(styleguideArg)
445
+ };
446
+ }
447
+ if (stat.isDirectory()) {
448
+ const found = findStyleGuidePath(styleguideArg);
449
+ return {
450
+ styleguideLocation: found,
451
+ styleGuide: found ? await readStyleGuide(found) : null
452
+ };
453
+ }
454
+ }
455
+ return { styleGuide: null, styleguideLocation: null };
456
+ }
457
+ const upwards = findUILintStyleGuideUpwards(startDir);
458
+ const fallback = upwards ?? findStyleGuidePath(projectPath);
459
+ return {
460
+ styleguideLocation: fallback,
461
+ styleGuide: fallback ? await readStyleGuide(fallback) : null
462
+ };
463
+ }
464
+ var ollamaReadyOnce = /* @__PURE__ */ new Map();
465
+ async function ensureOllamaReadyCached(params) {
466
+ const key = `${params.baseUrl}::${params.model}`;
467
+ const existing = ollamaReadyOnce.get(key);
468
+ if (existing) return existing;
469
+ const p = ensureOllamaReady({ model: params.model, baseUrl: params.baseUrl }).then(() => void 0).catch((e) => {
470
+ ollamaReadyOnce.delete(key);
471
+ throw e;
472
+ });
473
+ ollamaReadyOnce.set(key, p);
474
+ return p;
475
+ }
476
+ function writeVisionDebugDump(params) {
477
+ const resolvePath = params.pathResolver ?? defaultPathResolver;
478
+ const resolvedDirOrFile = resolvePath(params.dumpPath, process.cwd());
479
+ const safeStamp = params.now.toISOString().replace(/[:.]/g, "-");
480
+ const dumpFile = resolvedDirOrFile.endsWith(".json") || resolvedDirOrFile.endsWith(".jsonl") ? resolvedDirOrFile : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;
481
+ mkdirSync(dirname(dumpFile), { recursive: true });
482
+ writeFileSync(
483
+ dumpFile,
484
+ JSON.stringify(
485
+ {
486
+ version: 1,
487
+ timestamp: params.now.toISOString(),
488
+ runtime: params.runtime,
489
+ metadata: params.metadata ?? null,
490
+ inputs: {
491
+ imageBase64: params.includeSensitive ? params.inputs.imageBase64 : "(omitted; set debugDumpIncludeSensitive=true)",
492
+ manifest: params.inputs.manifest,
493
+ styleguideLocation: params.inputs.styleguideLocation,
494
+ styleGuide: params.includeSensitive ? params.inputs.styleGuide : "(omitted; set debugDumpIncludeSensitive=true)"
495
+ }
496
+ },
497
+ null,
498
+ 2
499
+ ),
500
+ "utf-8"
501
+ );
502
+ return dumpFile;
503
+ }
504
+ async function runVisionAnalysis(args) {
505
+ const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;
506
+ const baseUrl = args.baseUrl ?? "http://localhost:11434";
507
+ let styleGuide = null;
508
+ let styleguideLocation = null;
509
+ if (args.styleGuide !== void 0) {
510
+ styleGuide = args.styleGuide;
511
+ styleguideLocation = args.styleguideLocation ?? null;
512
+ } else {
513
+ args.onPhase?.("Resolving styleguide...");
514
+ const resolved = await resolveVisionStyleGuide({
515
+ projectPath: args.projectPath,
516
+ styleguide: args.styleguide,
517
+ startDir: args.styleguideStartDir,
518
+ pathResolver: args.pathResolver
519
+ });
520
+ styleGuide = resolved.styleGuide;
521
+ styleguideLocation = resolved.styleguideLocation;
522
+ }
523
+ if (!args.skipEnsureOllama) {
524
+ args.onPhase?.("Preparing Ollama...");
525
+ await ensureOllamaReadyCached({ model: visionModel, baseUrl });
526
+ }
527
+ if (args.debugDump) {
528
+ writeVisionDebugDump({
529
+ dumpPath: args.debugDump,
530
+ now: /* @__PURE__ */ new Date(),
531
+ runtime: { visionModel, baseUrl },
532
+ inputs: {
533
+ imageBase64: args.imageBase64,
534
+ manifest: args.manifest,
535
+ styleguideLocation,
536
+ styleGuide
537
+ },
538
+ includeSensitive: Boolean(args.debugDumpIncludeSensitive),
539
+ metadata: args.debugDumpMetadata,
540
+ pathResolver: args.pathResolver
541
+ });
542
+ }
543
+ const analyzer = args.analyzer ?? new VisionAnalyzer({
544
+ baseUrl: args.baseUrl,
545
+ visionModel
546
+ });
547
+ args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);
548
+ const result = await analyzer.analyzeScreenshot(
549
+ args.imageBase64,
550
+ args.manifest,
551
+ {
552
+ styleGuide,
553
+ onProgress: args.onProgress
554
+ }
555
+ );
556
+ args.onPhase?.(
557
+ `Done (${result.issues.length} issues, ${result.analysisTime}ms)`
558
+ );
559
+ return {
560
+ issues: result.issues,
561
+ analysisTime: result.analysisTime,
562
+ // Prompt is available in newer uilint-core versions; keep this resilient across versions.
563
+ prompt: result.prompt,
564
+ rawResponse: result.rawResponse,
565
+ styleguideLocation,
566
+ visionModel,
567
+ baseUrl
568
+ };
569
+ }
570
+ function writeVisionMarkdownReport(args) {
571
+ const p = parse(args.imagePath);
572
+ const outPath = args.outPath ?? join(p.dir, `${p.name || p.base}.vision.md`);
573
+ const lines = [];
574
+ lines.push(`# UILint Vision Report`);
575
+ lines.push(``);
576
+ lines.push(`- Image: \`${p.base}\``);
577
+ if (args.route) lines.push(`- Route: \`${args.route}\``);
578
+ if (typeof args.timestamp === "number") {
579
+ lines.push(`- Timestamp: \`${new Date(args.timestamp).toISOString()}\``);
580
+ }
581
+ if (args.visionModel) lines.push(`- Model: \`${args.visionModel}\``);
582
+ if (args.baseUrl) lines.push(`- Ollama baseUrl: \`${args.baseUrl}\``);
583
+ if (typeof args.analysisTimeMs === "number")
584
+ lines.push(`- Analysis time: \`${args.analysisTimeMs}ms\``);
585
+ lines.push(`- Generated: \`${(/* @__PURE__ */ new Date()).toISOString()}\``);
586
+ lines.push(``);
587
+ if (args.metadata && Object.keys(args.metadata).length > 0) {
588
+ lines.push(`## Metadata`);
589
+ lines.push(``);
590
+ lines.push("```json");
591
+ lines.push(JSON.stringify(args.metadata, null, 2));
592
+ lines.push("```");
593
+ lines.push(``);
594
+ }
595
+ lines.push(`## Prompt`);
596
+ lines.push(``);
597
+ lines.push("```text");
598
+ lines.push((args.prompt ?? "").trim());
599
+ lines.push("```");
600
+ lines.push(``);
601
+ lines.push(`## Raw Response`);
602
+ lines.push(``);
603
+ lines.push("```text");
604
+ lines.push((args.rawResponse ?? "").trim());
605
+ lines.push("```");
606
+ lines.push(``);
607
+ const content = lines.join("\n");
608
+ mkdirSync(dirname(outPath), { recursive: true });
609
+ writeFileSync(outPath, content, "utf-8");
610
+ return { outPath, content };
611
+ }
418
612
  export {
419
613
  DEFAULT_VISION_AUTO_SCAN_SETTINGS,
420
614
  MANIFEST_SKIP_TAGS,
@@ -433,6 +627,9 @@ export {
433
627
  getCurrentRoute,
434
628
  getVisionAnalyzer,
435
629
  matchIssuesToManifest,
436
- visionPlugin
630
+ resolveVisionStyleGuide,
631
+ runVisionAnalysis,
632
+ visionPlugin,
633
+ writeVisionMarkdownReport
437
634
  };
438
635
  //# sourceMappingURL=node.js.map
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/analyzer/vision-analyzer.ts"],"sourcesContent":["/**\n * Vision analyzer for Ollama vision LLM analysis\n *\n * This module provides functionality to analyze screenshots using Ollama's\n * vision models (e.g., qwen3-vl, llava) to detect UI consistency issues\n * that are only visible in rendered output.\n *\n * Uses the official ollama npm package with the chat API for proper vision support.\n */\n\nimport { Ollama } from \"ollama\";\nimport type {\n OllamaClientOptions,\n StreamProgressCallback,\n LLMInstrumentationCallbacks,\n InstrumentationSpan,\n} from \"uilint-core\";\n\nimport type {\n ElementManifest,\n VisionIssue,\n} from \"../types.js\";\n\n/**\n * Internal result type for the analyzer\n * (Different from VisionAnalysisResult which includes route/timestamp/manifest)\n */\nexport interface AnalyzerResult {\n /** Detected issues */\n issues: VisionIssue[];\n /** Analysis time in milliseconds */\n analysisTime: number;\n /** Full prompt sent to the model (for debugging/reports) */\n prompt?: string;\n /** Raw LLM response (for debugging) */\n rawResponse?: string;\n}\n\n/**\n * Default vision model - qwen3-vl is recommended for UI analysis\n */\nexport const UILINT_DEFAULT_VISION_MODEL = \"qwen3-vl:8b-instruct\";\n\nfunction stripCodeFences(input: string): string {\n const trimmed = (input ?? \"\").trim();\n if (!trimmed.startsWith(\"```\")) return trimmed;\n const m = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n return m ? m[1]!.trim() : trimmed;\n}\n\nfunction extractJsonObject(input: string): string {\n const s = stripCodeFences(input).trim();\n if (!s) return s;\n if (s.startsWith(\"{\") && s.endsWith(\"}\")) return s;\n const start = s.indexOf(\"{\");\n const end = s.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return s.slice(start, end + 1).trim();\n }\n return s;\n}\n\n/**\n * Options for vision analysis\n */\nexport interface VisionAnalysisOptions {\n /** Style guide content (markdown) */\n styleGuide?: string | null;\n /** Progress callback for streaming */\n onProgress?: StreamProgressCallback;\n /** Custom prompt additions */\n additionalContext?: string;\n}\n\n/**\n * Vision analyzer client options\n */\nexport interface VisionAnalyzerOptions extends OllamaClientOptions {\n /** Vision model to use (default: qwen3-vl:30b) */\n visionModel?: string;\n}\n\nconst DEFAULT_BASE_URL = \"http://localhost:11434\";\nconst DEFAULT_TIMEOUT = 120000; // Vision models can be slower\n\n/**\n * Vision analyzer for UI screenshot analysis\n * Uses the ollama npm package with chat API for proper vision model support\n */\nexport class VisionAnalyzer {\n private baseUrl: string;\n private visionModel: string;\n private timeout: number;\n private instrumentation?: LLMInstrumentationCallbacks;\n private client: Ollama;\n\n constructor(options: VisionAnalyzerOptions = {}) {\n this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n this.visionModel = options.visionModel || UILINT_DEFAULT_VISION_MODEL;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n this.instrumentation = options.instrumentation;\n this.client = new Ollama({ host: this.baseUrl });\n }\n\n /**\n * Analyzes a screenshot with element manifest for UI consistency issues\n */\n async analyzeScreenshot(\n imageBase64: string,\n manifest: ElementManifest[],\n options: VisionAnalysisOptions = {}\n ): Promise<AnalyzerResult> {\n const startTime = Date.now();\n\n const prompt = this.buildVisionPrompt(manifest, options);\n\n // Start instrumentation span if available\n const spanResult = this.instrumentation?.onGenerationStart?.({\n name: \"vision-analyze\",\n model: this.visionModel,\n prompt,\n metadata: {\n manifestSize: manifest.length,\n hasStyleGuide: !!options.styleGuide,\n },\n });\n // Convert void to undefined for type compatibility\n const span: InstrumentationSpan | undefined = spanResult || undefined;\n\n try {\n const rawResponse = options.onProgress\n ? await this.chatVisionStreaming(\n imageBase64,\n prompt,\n options.onProgress,\n span\n )\n : await this.chatVision(imageBase64, prompt, span);\n\n const issues = this.parseVisionResponse(rawResponse, manifest);\n\n return {\n issues,\n analysisTime: Date.now() - startTime,\n prompt,\n rawResponse,\n };\n } catch (error) {\n const analysisTime = Date.now() - startTime;\n const err =\n error instanceof Error\n ? error\n : new Error(String(error ?? \"Unknown error\"));\n console.error(\"[VisionAnalyzer] Analysis failed\", {\n baseUrl: this.baseUrl,\n model: this.visionModel,\n manifestSize: manifest.length,\n analysisTime,\n error: err.message,\n });\n span?.end(\"\", { error: err.message });\n throw err;\n }\n }\n\n /**\n * Builds the vision analysis prompt\n */\n private buildVisionPrompt(\n manifest: ElementManifest[],\n options: VisionAnalysisOptions\n ): string {\n const styleGuideSection = options.styleGuide\n ? `## Style Guide\n${options.styleGuide}\n\n`\n : \"\";\n\n const additionalSection = options.additionalContext\n ? `## Additional Context\n${options.additionalContext}\n\n`\n : \"\";\n\n // Build element manifest section for context\n const manifestSection = this.buildManifestSection(manifest);\n\n return `You are a UI consistency analyzer examining a screenshot of a web page.\nAnalyze the visual appearance and identify any UI consistency issues.\n\n${styleGuideSection}${additionalSection}${manifestSection}\n\n## Task\n\nExamine the screenshot and identify visual consistency issues such as:\n1. **Spacing inconsistencies**: Elements with uneven padding, margins, or gaps\n2. **Alignment issues**: Elements that should be aligned but aren't\n3. **Color inconsistencies**: Similar colors that should be identical, or colors that don't match the style guide\n4. **Typography issues**: Inconsistent font sizes, weights, or families\n5. **Layout problems**: Broken layouts, overlapping elements, or visual hierarchy issues\n6. **Contrast issues**: Text that's hard to read or UI elements with insufficient contrast\n\nFor each issue found, reference the element by its visible text so we can map it back to the source code.\n\n## Response Format\n\nRespond with JSON ONLY. Return a single JSON object:\n\n\\`\\`\\`json\n{\n \"issues\": [\n {\n \"elementText\": \"Submit Order\",\n \"message\": \"Button has inconsistent padding compared to other buttons (appears larger)\",\n \"category\": \"spacing\",\n \"severity\": \"warning\"\n }\n ]\n}\n\\`\\`\\`\n\nCategories: spacing, alignment, color, typography, layout, contrast, visual-hierarchy\nSeverities: error (major issue), warning (should fix), info (minor/suggestion)\n\nIMPORTANT:\n- Reference elements by their visible text content\n- Be specific about what's wrong and what the expected state should be\n- Only report significant visual inconsistencies\n- If no issues are found, return {\"issues\": []}`;\n }\n\n /**\n * Builds the manifest section of the prompt\n */\n private buildManifestSection(manifest: ElementManifest[]): string {\n if (manifest.length === 0) {\n return \"\";\n }\n\n const elements = manifest.map((el) => {\n const parts = [`- \"${el.text}\"`];\n if (el.role) parts.push(`(${el.role})`);\n if (el.instanceCount && el.instanceCount > 1) {\n parts.push(`[${el.instanceCount} instances]`);\n }\n return parts.join(\" \");\n });\n\n return `## Page Elements\n\nThe following elements are visible on the page (reference by text when reporting issues):\n\n${elements.join(\"\\n\")}\n\n`;\n }\n\n /**\n * Non-streaming chat API call for vision\n */\n private async chatVision(\n imageBase64: string,\n prompt: string,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const response = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n format: \"json\",\n options: {\n // Increase context window for large prompts\n num_ctx: 8192,\n },\n });\n\n const output = response.message?.content || \"\";\n\n if (!output.trim()) {\n const diag = JSON.stringify(\n {\n done_reason: response.done_reason,\n model: response.model,\n prompt_eval_count: response.prompt_eval_count,\n eval_count: response.eval_count,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output. Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(output, {\n promptTokens: response.prompt_eval_count,\n completionTokens: response.eval_count,\n totalTokens:\n (response.prompt_eval_count || 0) + (response.eval_count || 0) ||\n undefined,\n });\n\n return output;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Streaming chat API call for vision\n * Uses ollama package's async iterator for streaming\n */\n private async chatVisionStreaming(\n imageBase64: string,\n prompt: string,\n onProgress: StreamProgressCallback,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n let promptTokens: number | undefined;\n let completionTokens: number | undefined;\n let chunksReceived = 0;\n let lastProgressAt = Date.now();\n let emittedWaitingLine = false;\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const stream = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n // Enable thinking for streaming so we can surface reasoning traces for thinking-capable models.\n // Models that don't support it should ignore it.\n think: false,\n stream: true,\n format: \"json\",\n options: {\n num_ctx: 8192,\n },\n });\n\n let fullResponse = \"\";\n let lastLatestLine = \"\";\n\n for await (const chunk of stream) {\n chunksReceived++;\n\n // Emit waiting message on first chunk if no content yet\n if (!emittedWaitingLine && !fullResponse.trim()) {\n onProgress(\"(waiting for model output…)\", fullResponse);\n emittedWaitingLine = true;\n }\n\n // Thinking-capable models stream `message.thinking` separately from `message.content`.\n const thinking = (chunk as any)?.message?.thinking || \"\";\n if (thinking) {\n onProgress(lastLatestLine, fullResponse, undefined, thinking);\n lastProgressAt = Date.now();\n }\n\n // Chat API uses message.content for text deltas (final answer)\n const content = chunk.message?.content || \"\";\n if (content) {\n fullResponse += content;\n\n // Get the latest line for progress display\n const responseLines = fullResponse.split(\"\\n\");\n const latestLine =\n responseLines[responseLines.length - 1] ||\n responseLines[responseLines.length - 2] ||\n \"\";\n\n // Stream the actual text delta to the callback\n lastLatestLine = latestLine.trim();\n onProgress(lastLatestLine, fullResponse, content);\n lastProgressAt = Date.now();\n }\n\n // Capture token counts from final chunk\n if (chunk.done) {\n promptTokens = chunk.prompt_eval_count;\n completionTokens = chunk.eval_count;\n }\n\n // Heartbeat if we're receiving chunks but no text output\n if (!fullResponse.trim() && Date.now() - lastProgressAt > 4000) {\n onProgress(\n `(streaming… received ${chunksReceived} chunks)`,\n fullResponse\n );\n lastProgressAt = Date.now();\n }\n }\n\n if (!fullResponse.trim()) {\n const diag = JSON.stringify(\n {\n chunksReceived,\n promptTokens,\n completionTokens,\n model: this.visionModel,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output (streaming). Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(fullResponse, {\n promptTokens,\n completionTokens,\n totalTokens: (promptTokens || 0) + (completionTokens || 0) || undefined,\n });\n\n return fullResponse;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parses the vision LLM response and matches elements to manifest\n */\n private parseVisionResponse(\n response: string,\n manifest: ElementManifest[]\n ): VisionIssue[] {\n try {\n const jsonText = extractJsonObject(response);\n const parsed = JSON.parse(jsonText);\n const rawIssues = parsed.issues || [];\n\n // Map elementText to dataLoc using manifest\n return rawIssues.map((issue: VisionIssue) => {\n const matchedElement = this.matchElementByText(\n issue.elementText,\n manifest\n );\n\n return {\n ...issue,\n dataLoc: matchedElement?.dataLoc,\n };\n });\n } catch {\n const s = response ?? \"\";\n const previewHead = s ? s.slice(0, 600) : \"\";\n const previewTail = s && s.length > 600 ? s.slice(-400) : \"\";\n throw new Error(\n `Vision model returned non-JSON output (expected JSON). length=${\n s.length\n } head=${JSON.stringify(previewHead)} tail=${JSON.stringify(\n previewTail\n )}`\n );\n }\n }\n\n /**\n * Matches element text from LLM response to manifest entries\n */\n private matchElementByText(\n elementText: string,\n manifest: ElementManifest[]\n ): ElementManifest | undefined {\n if (!elementText) return undefined;\n\n const normalizedSearch = elementText.toLowerCase().trim();\n\n // Exact match first\n for (const entry of manifest) {\n if (entry.text.toLowerCase().trim() === normalizedSearch) {\n return entry;\n }\n }\n\n // Partial match\n for (const entry of manifest) {\n const normalizedEntry = entry.text.toLowerCase().trim();\n if (\n normalizedEntry.includes(normalizedSearch) ||\n normalizedSearch.includes(normalizedEntry)\n ) {\n return entry;\n }\n }\n\n return undefined;\n }\n\n /**\n * Checks if the vision model is available\n */\n async isAvailable(): Promise<boolean> {\n try {\n const models = await this.client.list();\n return models.models.some(\n (m) =>\n m.name === this.visionModel ||\n m.name.startsWith(this.visionModel.split(\":\")[0])\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Gets the current vision model\n */\n getModel(): string {\n return this.visionModel;\n }\n\n /**\n * Gets the Ollama base URL (for diagnostics)\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n\n /**\n * Sets the vision model\n */\n setModel(model: string): void {\n this.visionModel = model;\n }\n\n /**\n * Sets instrumentation callbacks\n */\n setInstrumentation(\n instrumentation: LLMInstrumentationCallbacks | undefined\n ): void {\n this.instrumentation = instrumentation;\n }\n}\n\n// Default singleton instance\nlet defaultAnalyzer: VisionAnalyzer | null = null;\n\n/**\n * Gets the default VisionAnalyzer instance\n */\nexport function getVisionAnalyzer(\n options?: VisionAnalyzerOptions\n): VisionAnalyzer {\n if (!defaultAnalyzer || options) {\n defaultAnalyzer = new VisionAnalyzer(options);\n }\n return defaultAnalyzer;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/analyzer/vision-analyzer.ts","../src/vision-run.ts"],"sourcesContent":["/**\n * Vision analyzer for Ollama vision LLM analysis\n *\n * This module provides functionality to analyze screenshots using Ollama's\n * vision models (e.g., qwen3-vl, llava) to detect UI consistency issues\n * that are only visible in rendered output.\n *\n * Uses the official ollama npm package with the chat API for proper vision support.\n */\n\nimport { Ollama } from \"ollama\";\nimport type {\n OllamaClientOptions,\n StreamProgressCallback,\n LLMInstrumentationCallbacks,\n InstrumentationSpan,\n} from \"uilint-core\";\n\nimport type {\n ElementManifest,\n VisionIssue,\n} from \"../types.js\";\n\n/**\n * Internal result type for the analyzer\n * (Different from VisionAnalysisResult which includes route/timestamp/manifest)\n */\nexport interface AnalyzerResult {\n /** Detected issues */\n issues: VisionIssue[];\n /** Analysis time in milliseconds */\n analysisTime: number;\n /** Full prompt sent to the model (for debugging/reports) */\n prompt?: string;\n /** Raw LLM response (for debugging) */\n rawResponse?: string;\n}\n\n/**\n * Default vision model - qwen3-vl is recommended for UI analysis\n */\nexport const UILINT_DEFAULT_VISION_MODEL = \"qwen3-vl:8b-instruct\";\n\nfunction stripCodeFences(input: string): string {\n const trimmed = (input ?? \"\").trim();\n if (!trimmed.startsWith(\"```\")) return trimmed;\n const m = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n return m ? m[1]!.trim() : trimmed;\n}\n\nfunction extractJsonObject(input: string): string {\n const s = stripCodeFences(input).trim();\n if (!s) return s;\n if (s.startsWith(\"{\") && s.endsWith(\"}\")) return s;\n const start = s.indexOf(\"{\");\n const end = s.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return s.slice(start, end + 1).trim();\n }\n return s;\n}\n\n/**\n * Options for vision analysis\n */\nexport interface VisionAnalysisOptions {\n /** Style guide content (markdown) */\n styleGuide?: string | null;\n /** Progress callback for streaming */\n onProgress?: StreamProgressCallback;\n /** Custom prompt additions */\n additionalContext?: string;\n}\n\n/**\n * Vision analyzer client options\n */\nexport interface VisionAnalyzerOptions extends OllamaClientOptions {\n /** Vision model to use (default: qwen3-vl:30b) */\n visionModel?: string;\n}\n\nconst DEFAULT_BASE_URL = \"http://localhost:11434\";\nconst DEFAULT_TIMEOUT = 120000; // Vision models can be slower\n\n/**\n * Vision analyzer for UI screenshot analysis\n * Uses the ollama npm package with chat API for proper vision model support\n */\nexport class VisionAnalyzer {\n private baseUrl: string;\n private visionModel: string;\n private timeout: number;\n private instrumentation?: LLMInstrumentationCallbacks;\n private client: Ollama;\n\n constructor(options: VisionAnalyzerOptions = {}) {\n this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n this.visionModel = options.visionModel || UILINT_DEFAULT_VISION_MODEL;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n this.instrumentation = options.instrumentation;\n this.client = new Ollama({ host: this.baseUrl });\n }\n\n /**\n * Analyzes a screenshot with element manifest for UI consistency issues\n */\n async analyzeScreenshot(\n imageBase64: string,\n manifest: ElementManifest[],\n options: VisionAnalysisOptions = {}\n ): Promise<AnalyzerResult> {\n const startTime = Date.now();\n\n const prompt = this.buildVisionPrompt(manifest, options);\n\n // Start instrumentation span if available\n const spanResult = this.instrumentation?.onGenerationStart?.({\n name: \"vision-analyze\",\n model: this.visionModel,\n prompt,\n metadata: {\n manifestSize: manifest.length,\n hasStyleGuide: !!options.styleGuide,\n },\n });\n // Convert void to undefined for type compatibility\n const span: InstrumentationSpan | undefined = spanResult || undefined;\n\n try {\n const rawResponse = options.onProgress\n ? await this.chatVisionStreaming(\n imageBase64,\n prompt,\n options.onProgress,\n span\n )\n : await this.chatVision(imageBase64, prompt, span);\n\n const issues = this.parseVisionResponse(rawResponse, manifest);\n\n return {\n issues,\n analysisTime: Date.now() - startTime,\n prompt,\n rawResponse,\n };\n } catch (error) {\n const analysisTime = Date.now() - startTime;\n const err =\n error instanceof Error\n ? error\n : new Error(String(error ?? \"Unknown error\"));\n console.error(\"[VisionAnalyzer] Analysis failed\", {\n baseUrl: this.baseUrl,\n model: this.visionModel,\n manifestSize: manifest.length,\n analysisTime,\n error: err.message,\n });\n span?.end(\"\", { error: err.message });\n throw err;\n }\n }\n\n /**\n * Builds the vision analysis prompt\n */\n private buildVisionPrompt(\n manifest: ElementManifest[],\n options: VisionAnalysisOptions\n ): string {\n const styleGuideSection = options.styleGuide\n ? `## Style Guide\n${options.styleGuide}\n\n`\n : \"\";\n\n const additionalSection = options.additionalContext\n ? `## Additional Context\n${options.additionalContext}\n\n`\n : \"\";\n\n // Build element manifest section for context\n const manifestSection = this.buildManifestSection(manifest);\n\n return `You are a UI consistency analyzer examining a screenshot of a web page.\nAnalyze the visual appearance and identify any UI consistency issues.\n\n${styleGuideSection}${additionalSection}${manifestSection}\n\n## Task\n\nExamine the screenshot and identify visual consistency issues such as:\n1. **Spacing inconsistencies**: Elements with uneven padding, margins, or gaps\n2. **Alignment issues**: Elements that should be aligned but aren't\n3. **Color inconsistencies**: Similar colors that should be identical, or colors that don't match the style guide\n4. **Typography issues**: Inconsistent font sizes, weights, or families\n5. **Layout problems**: Broken layouts, overlapping elements, or visual hierarchy issues\n6. **Contrast issues**: Text that's hard to read or UI elements with insufficient contrast\n\nFor each issue found, reference the element by its visible text so we can map it back to the source code.\n\n## Response Format\n\nRespond with JSON ONLY. Return a single JSON object:\n\n\\`\\`\\`json\n{\n \"issues\": [\n {\n \"elementText\": \"Submit Order\",\n \"message\": \"Button has inconsistent padding compared to other buttons (appears larger)\",\n \"category\": \"spacing\",\n \"severity\": \"warning\"\n }\n ]\n}\n\\`\\`\\`\n\nCategories: spacing, alignment, color, typography, layout, contrast, visual-hierarchy\nSeverities: error (major issue), warning (should fix), info (minor/suggestion)\n\nIMPORTANT:\n- Reference elements by their visible text content\n- Be specific about what's wrong and what the expected state should be\n- Only report significant visual inconsistencies\n- If no issues are found, return {\"issues\": []}`;\n }\n\n /**\n * Builds the manifest section of the prompt\n */\n private buildManifestSection(manifest: ElementManifest[]): string {\n if (manifest.length === 0) {\n return \"\";\n }\n\n const elements = manifest.map((el) => {\n const parts = [`- \"${el.text}\"`];\n if (el.role) parts.push(`(${el.role})`);\n if (el.instanceCount && el.instanceCount > 1) {\n parts.push(`[${el.instanceCount} instances]`);\n }\n return parts.join(\" \");\n });\n\n return `## Page Elements\n\nThe following elements are visible on the page (reference by text when reporting issues):\n\n${elements.join(\"\\n\")}\n\n`;\n }\n\n /**\n * Non-streaming chat API call for vision\n */\n private async chatVision(\n imageBase64: string,\n prompt: string,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const response = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n format: \"json\",\n options: {\n // Increase context window for large prompts\n num_ctx: 8192,\n },\n });\n\n const output = response.message?.content || \"\";\n\n if (!output.trim()) {\n const diag = JSON.stringify(\n {\n done_reason: response.done_reason,\n model: response.model,\n prompt_eval_count: response.prompt_eval_count,\n eval_count: response.eval_count,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output. Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(output, {\n promptTokens: response.prompt_eval_count,\n completionTokens: response.eval_count,\n totalTokens:\n (response.prompt_eval_count || 0) + (response.eval_count || 0) ||\n undefined,\n });\n\n return output;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Streaming chat API call for vision\n * Uses ollama package's async iterator for streaming\n */\n private async chatVisionStreaming(\n imageBase64: string,\n prompt: string,\n onProgress: StreamProgressCallback,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n let promptTokens: number | undefined;\n let completionTokens: number | undefined;\n let chunksReceived = 0;\n let lastProgressAt = Date.now();\n let emittedWaitingLine = false;\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const stream = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n // Enable thinking for streaming so we can surface reasoning traces for thinking-capable models.\n // Models that don't support it should ignore it.\n think: false,\n stream: true,\n format: \"json\",\n options: {\n num_ctx: 8192,\n },\n });\n\n let fullResponse = \"\";\n let lastLatestLine = \"\";\n\n for await (const chunk of stream) {\n chunksReceived++;\n\n // Emit waiting message on first chunk if no content yet\n if (!emittedWaitingLine && !fullResponse.trim()) {\n onProgress(\"(waiting for model output…)\", fullResponse);\n emittedWaitingLine = true;\n }\n\n // Thinking-capable models stream `message.thinking` separately from `message.content`.\n const thinking = (chunk as any)?.message?.thinking || \"\";\n if (thinking) {\n onProgress(lastLatestLine, fullResponse, undefined, thinking);\n lastProgressAt = Date.now();\n }\n\n // Chat API uses message.content for text deltas (final answer)\n const content = chunk.message?.content || \"\";\n if (content) {\n fullResponse += content;\n\n // Get the latest line for progress display\n const responseLines = fullResponse.split(\"\\n\");\n const latestLine =\n responseLines[responseLines.length - 1] ||\n responseLines[responseLines.length - 2] ||\n \"\";\n\n // Stream the actual text delta to the callback\n lastLatestLine = latestLine.trim();\n onProgress(lastLatestLine, fullResponse, content);\n lastProgressAt = Date.now();\n }\n\n // Capture token counts from final chunk\n if (chunk.done) {\n promptTokens = chunk.prompt_eval_count;\n completionTokens = chunk.eval_count;\n }\n\n // Heartbeat if we're receiving chunks but no text output\n if (!fullResponse.trim() && Date.now() - lastProgressAt > 4000) {\n onProgress(\n `(streaming… received ${chunksReceived} chunks)`,\n fullResponse\n );\n lastProgressAt = Date.now();\n }\n }\n\n if (!fullResponse.trim()) {\n const diag = JSON.stringify(\n {\n chunksReceived,\n promptTokens,\n completionTokens,\n model: this.visionModel,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output (streaming). Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(fullResponse, {\n promptTokens,\n completionTokens,\n totalTokens: (promptTokens || 0) + (completionTokens || 0) || undefined,\n });\n\n return fullResponse;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parses the vision LLM response and matches elements to manifest\n */\n private parseVisionResponse(\n response: string,\n manifest: ElementManifest[]\n ): VisionIssue[] {\n try {\n const jsonText = extractJsonObject(response);\n const parsed = JSON.parse(jsonText);\n const rawIssues = parsed.issues || [];\n\n // Map elementText to dataLoc using manifest\n return rawIssues.map((issue: VisionIssue) => {\n const matchedElement = this.matchElementByText(\n issue.elementText,\n manifest\n );\n\n return {\n ...issue,\n dataLoc: matchedElement?.dataLoc,\n };\n });\n } catch {\n const s = response ?? \"\";\n const previewHead = s ? s.slice(0, 600) : \"\";\n const previewTail = s && s.length > 600 ? s.slice(-400) : \"\";\n throw new Error(\n `Vision model returned non-JSON output (expected JSON). length=${\n s.length\n } head=${JSON.stringify(previewHead)} tail=${JSON.stringify(\n previewTail\n )}`\n );\n }\n }\n\n /**\n * Matches element text from LLM response to manifest entries\n */\n private matchElementByText(\n elementText: string,\n manifest: ElementManifest[]\n ): ElementManifest | undefined {\n if (!elementText) return undefined;\n\n const normalizedSearch = elementText.toLowerCase().trim();\n\n // Exact match first\n for (const entry of manifest) {\n if (entry.text.toLowerCase().trim() === normalizedSearch) {\n return entry;\n }\n }\n\n // Partial match\n for (const entry of manifest) {\n const normalizedEntry = entry.text.toLowerCase().trim();\n if (\n normalizedEntry.includes(normalizedSearch) ||\n normalizedSearch.includes(normalizedEntry)\n ) {\n return entry;\n }\n }\n\n return undefined;\n }\n\n /**\n * Checks if the vision model is available\n */\n async isAvailable(): Promise<boolean> {\n try {\n const models = await this.client.list();\n return models.models.some(\n (m) =>\n m.name === this.visionModel ||\n m.name.startsWith(this.visionModel.split(\":\")[0])\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Gets the current vision model\n */\n getModel(): string {\n return this.visionModel;\n }\n\n /**\n * Gets the Ollama base URL (for diagnostics)\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n\n /**\n * Sets the vision model\n */\n setModel(model: string): void {\n this.visionModel = model;\n }\n\n /**\n * Sets instrumentation callbacks\n */\n setInstrumentation(\n instrumentation: LLMInstrumentationCallbacks | undefined\n ): void {\n this.instrumentation = instrumentation;\n }\n}\n\n// Default singleton instance\nlet defaultAnalyzer: VisionAnalyzer | null = null;\n\n/**\n * Gets the default VisionAnalyzer instance\n */\nexport function getVisionAnalyzer(\n options?: VisionAnalyzerOptions\n): VisionAnalyzer {\n if (!defaultAnalyzer || options) {\n defaultAnalyzer = new VisionAnalyzer(options);\n }\n return defaultAnalyzer;\n}\n","/**\n * Vision analysis utilities for running vision analysis with Ollama.\n *\n * These utilities provide high-level orchestration for vision analysis,\n * including style guide resolution, Ollama readiness checks, and debug dump generation.\n */\n\nimport { dirname, join, parse, resolve } from \"path\";\nimport { existsSync, statSync, mkdirSync, writeFileSync } from \"fs\";\nimport {\n ensureOllamaReady,\n findStyleGuidePath,\n findUILintStyleGuideUpwards,\n readStyleGuide,\n type StreamProgressCallback,\n} from \"uilint-core/node\";\nimport {\n VisionAnalyzer,\n UILINT_DEFAULT_VISION_MODEL,\n type AnalyzerResult,\n} from \"./analyzer/vision-analyzer.js\";\nimport type { ElementManifest, VisionIssue } from \"./types.js\";\n\n/**\n * Path resolver function type.\n * Resolves a path specifier (which may include workspace-relative \"@\" prefixes or other formats)\n * into an absolute filesystem path.\n */\nexport type PathResolver = (spec: string, cwd: string) => string;\n\n/**\n * Default path resolver that just uses path.resolve.\n * CLI callers can provide a custom resolver that handles workspace-relative paths.\n */\nconst defaultPathResolver: PathResolver = (spec: string, cwd: string) => {\n const raw = (spec ?? \"\").trim();\n if (!raw) return resolve(cwd, spec);\n return resolve(cwd, raw);\n};\n\nexport type ResolveVisionStyleGuideArgs = {\n /** Project root / cwd used to resolve relative path specifiers */\n projectPath: string;\n /** Path to style guide file OR project directory */\n styleguide?: string;\n /** A starting point for upward search (directory). Defaults to projectPath. */\n startDir?: string;\n /** Optional custom path resolver (for handling workspace-relative paths like \"@apps/...\") */\n pathResolver?: PathResolver;\n};\n\nexport type ResolveVisionStyleGuideResult = {\n styleGuide: string | null;\n styleguideLocation: string | null;\n};\n\nexport async function resolveVisionStyleGuide(\n args: ResolveVisionStyleGuideArgs\n): Promise<ResolveVisionStyleGuideResult> {\n const projectPath = args.projectPath;\n const startDir = args.startDir ?? projectPath;\n const resolvePath = args.pathResolver ?? defaultPathResolver;\n\n if (args.styleguide) {\n const styleguideArg = resolvePath(args.styleguide, projectPath);\n if (existsSync(styleguideArg)) {\n const stat = statSync(styleguideArg);\n if (stat.isFile()) {\n return {\n styleguideLocation: styleguideArg,\n styleGuide: await readStyleGuide(styleguideArg),\n };\n }\n if (stat.isDirectory()) {\n const found = findStyleGuidePath(styleguideArg);\n return {\n styleguideLocation: found,\n styleGuide: found ? await readStyleGuide(found) : null,\n };\n }\n }\n return { styleGuide: null, styleguideLocation: null };\n }\n\n const upwards = findUILintStyleGuideUpwards(startDir);\n const fallback = upwards ?? findStyleGuidePath(projectPath);\n return {\n styleguideLocation: fallback,\n styleGuide: fallback ? await readStyleGuide(fallback) : null,\n };\n}\n\nexport type RunVisionAnalysisArgs = {\n /** base64 image data (NO data: prefix) */\n imageBase64: string;\n manifest: ElementManifest[];\n /** Used to resolve styleguide arg and as default search root */\n projectPath: string;\n /** Path to style guide file OR directory; if omitted uses upward search */\n styleguide?: string;\n /** Directory for upward styleguide search; defaults to projectPath */\n styleguideStartDir?: string;\n /**\n * Override resolved styleguide content (lets callers do logging/notes based on resolution once).\n * If provided (even null), styleguide resolution is skipped.\n */\n styleGuide?: string | null;\n /**\n * Override resolved styleguide location (pairs with styleGuide override).\n * If provided, this is returned in the result.\n */\n styleguideLocation?: string | null;\n /** Ollama base URL (default: http://localhost:11434) */\n baseUrl?: string;\n /** Vision model override (default: UILINT_DEFAULT_VISION_MODEL) */\n model?: string;\n /**\n * Optional analyzer instance (lets servers reuse a singleton and lets tests inject a mock).\n * If omitted, a new `VisionAnalyzer` is created with baseUrl+model.\n */\n analyzer?: Pick<VisionAnalyzer, \"analyzeScreenshot\">;\n /** Optional streaming progress callback passed into the analyzer */\n onProgress?: StreamProgressCallback;\n /** Optional coarse-grained phase callback (good for spinners / WS progress) */\n onPhase?: (phase: string) => void;\n /** When true, skip calling `ensureOllamaReady` (caller is responsible). */\n skipEnsureOllama?: boolean;\n /** Optional debug dump destination (file path or directory) */\n debugDump?: string;\n /** When true, include base64 image and full styleguide in debug dump */\n debugDumpIncludeSensitive?: boolean;\n /** Optional extra metadata to include in debug dumps */\n debugDumpMetadata?: Record<string, unknown>;\n /** Optional custom path resolver (for handling workspace-relative paths like \"@apps/...\") */\n pathResolver?: PathResolver;\n};\n\nexport type RunVisionAnalysisResult = {\n issues: VisionIssue[];\n analysisTime: number;\n prompt?: string;\n rawResponse?: string;\n styleguideLocation: string | null;\n visionModel: string;\n baseUrl: string;\n};\n\nconst ollamaReadyOnce = new Map<string, Promise<void>>();\n\nasync function ensureOllamaReadyCached(params: {\n model: string;\n baseUrl: string;\n}): Promise<void> {\n const key = `${params.baseUrl}::${params.model}`;\n const existing = ollamaReadyOnce.get(key);\n if (existing) return existing;\n\n const p = ensureOllamaReady({ model: params.model, baseUrl: params.baseUrl })\n .then(() => undefined)\n .catch((e) => {\n // If startup/pull fails, allow retry on next request.\n ollamaReadyOnce.delete(key);\n throw e;\n });\n ollamaReadyOnce.set(key, p);\n return p;\n}\n\nfunction writeVisionDebugDump(params: {\n dumpPath: string;\n now: Date;\n inputs: {\n imageBase64: string;\n manifest: ElementManifest[];\n styleguideLocation: string | null;\n styleGuide: string | null;\n };\n runtime: { visionModel: string; baseUrl: string };\n includeSensitive: boolean;\n metadata?: Record<string, unknown>;\n pathResolver?: PathResolver;\n}): string {\n const resolvePath = params.pathResolver ?? defaultPathResolver;\n const resolvedDirOrFile = resolvePath(params.dumpPath, process.cwd());\n const safeStamp = params.now.toISOString().replace(/[:.]/g, \"-\");\n const dumpFile =\n resolvedDirOrFile.endsWith(\".json\") || resolvedDirOrFile.endsWith(\".jsonl\")\n ? resolvedDirOrFile\n : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;\n\n mkdirSync(dirname(dumpFile), { recursive: true });\n writeFileSync(\n dumpFile,\n JSON.stringify(\n {\n version: 1,\n timestamp: params.now.toISOString(),\n runtime: params.runtime,\n metadata: params.metadata ?? null,\n inputs: {\n imageBase64: params.includeSensitive\n ? params.inputs.imageBase64\n : \"(omitted; set debugDumpIncludeSensitive=true)\",\n manifest: params.inputs.manifest,\n styleguideLocation: params.inputs.styleguideLocation,\n styleGuide: params.includeSensitive\n ? params.inputs.styleGuide\n : \"(omitted; set debugDumpIncludeSensitive=true)\",\n },\n },\n null,\n 2\n ),\n \"utf-8\"\n );\n\n return dumpFile;\n}\n\nexport async function runVisionAnalysis(\n args: RunVisionAnalysisArgs\n): Promise<RunVisionAnalysisResult> {\n const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;\n const baseUrl = args.baseUrl ?? \"http://localhost:11434\";\n\n let styleGuide: string | null = null;\n let styleguideLocation: string | null = null;\n\n if (args.styleGuide !== undefined) {\n styleGuide = args.styleGuide;\n styleguideLocation = args.styleguideLocation ?? null;\n } else {\n args.onPhase?.(\"Resolving styleguide...\");\n const resolved = await resolveVisionStyleGuide({\n projectPath: args.projectPath,\n styleguide: args.styleguide,\n startDir: args.styleguideStartDir,\n pathResolver: args.pathResolver,\n });\n styleGuide = resolved.styleGuide;\n styleguideLocation = resolved.styleguideLocation;\n }\n\n if (!args.skipEnsureOllama) {\n args.onPhase?.(\"Preparing Ollama...\");\n await ensureOllamaReadyCached({ model: visionModel, baseUrl });\n }\n\n if (args.debugDump) {\n writeVisionDebugDump({\n dumpPath: args.debugDump,\n now: new Date(),\n runtime: { visionModel, baseUrl },\n inputs: {\n imageBase64: args.imageBase64,\n manifest: args.manifest,\n styleguideLocation,\n styleGuide,\n },\n includeSensitive: Boolean(args.debugDumpIncludeSensitive),\n metadata: args.debugDumpMetadata,\n pathResolver: args.pathResolver,\n });\n }\n\n const analyzer =\n args.analyzer ??\n new VisionAnalyzer({\n baseUrl: args.baseUrl,\n visionModel,\n });\n\n args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);\n const result: AnalyzerResult = await analyzer.analyzeScreenshot(\n args.imageBase64,\n args.manifest,\n {\n styleGuide,\n onProgress: args.onProgress,\n }\n );\n\n args.onPhase?.(\n `Done (${result.issues.length} issues, ${result.analysisTime}ms)`\n );\n\n return {\n issues: result.issues,\n analysisTime: result.analysisTime,\n // Prompt is available in newer uilint-core versions; keep this resilient across versions.\n prompt: (result as any).prompt,\n rawResponse: result.rawResponse,\n styleguideLocation,\n visionModel,\n baseUrl,\n };\n}\n\nexport type WriteVisionMarkdownReportArgs = {\n /** Absolute path to the source image file */\n imagePath: string;\n /** Route label (optional; included in report) */\n route?: string;\n /** Unix ms timestamp (optional; included in report) */\n timestamp?: number;\n visionModel?: string;\n baseUrl?: string;\n analysisTimeMs?: number;\n prompt?: string | null;\n rawResponse?: string | null;\n /** Extra JSON-ish metadata to include */\n metadata?: Record<string, unknown>;\n /**\n * Optional output path; if omitted, writes alongside the image:\n * `<basename>.vision.md`\n */\n outPath?: string;\n};\n\nexport function writeVisionMarkdownReport(\n args: WriteVisionMarkdownReportArgs\n): {\n outPath: string;\n content: string;\n} {\n const p = parse(args.imagePath);\n const outPath = args.outPath ?? join(p.dir, `${p.name || p.base}.vision.md`);\n\n const lines: string[] = [];\n lines.push(`# UILint Vision Report`);\n lines.push(``);\n lines.push(`- Image: \\`${p.base}\\``);\n if (args.route) lines.push(`- Route: \\`${args.route}\\``);\n if (typeof args.timestamp === \"number\") {\n lines.push(`- Timestamp: \\`${new Date(args.timestamp).toISOString()}\\``);\n }\n if (args.visionModel) lines.push(`- Model: \\`${args.visionModel}\\``);\n if (args.baseUrl) lines.push(`- Ollama baseUrl: \\`${args.baseUrl}\\``);\n if (typeof args.analysisTimeMs === \"number\")\n lines.push(`- Analysis time: \\`${args.analysisTimeMs}ms\\``);\n lines.push(`- Generated: \\`${new Date().toISOString()}\\``);\n lines.push(``);\n\n if (args.metadata && Object.keys(args.metadata).length > 0) {\n lines.push(`## Metadata`);\n lines.push(``);\n lines.push(\"```json\");\n lines.push(JSON.stringify(args.metadata, null, 2));\n lines.push(\"```\");\n lines.push(``);\n }\n\n lines.push(`## Prompt`);\n lines.push(``);\n lines.push(\"```text\");\n lines.push((args.prompt ?? \"\").trim());\n lines.push(\"```\");\n lines.push(``);\n\n lines.push(`## Raw Response`);\n lines.push(``);\n lines.push(\"```text\");\n lines.push((args.rawResponse ?? \"\").trim());\n lines.push(\"```\");\n lines.push(``);\n\n const content = lines.join(\"\\n\");\n mkdirSync(dirname(outPath), { recursive: true });\n writeFileSync(outPath, content, \"utf-8\");\n return { outPath, content };\n}\n"],"mappings":"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","names":[]}
@@ -51,4 +51,4 @@ interface VisionState {
51
51
  */
52
52
  declare const visionPlugin: PluginWithHandlers<VisionState>;
53
53
 
54
- export { visionPlugin as default, visionPlugin };
54
+ export { type VisionState, visionPlugin as default, visionPlugin };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  plugin_default,
3
3
  visionPlugin
4
- } from "../chunk-CM75UYJ5.js";
4
+ } from "../chunk-GFLTUBO7.js";
5
5
  export {
6
6
  plugin_default as default,
7
7
  visionPlugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uilint-vision",
3
- "version": "0.2.136",
3
+ "version": "0.2.137",
4
4
  "description": "Vision-based UI consistency analysis for UILint",
5
5
  "author": "Peter Suggate",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "dist"
35
35
  ],
36
36
  "peerDependencies": {
37
- "uilint-core": "0.2.136"
37
+ "uilint-core": "0.2.137"
38
38
  },
39
39
  "dependencies": {
40
40
  "html-to-image": "^1.11.13"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/plugin/index.ts","../src/types.ts","../src/plugin/state.ts","../src/plugin/actions.ts","../src/plugin/commands.ts","../src/plugin/toolbar.ts","../src/plugin/panels.ts","../src/plugin/rules.ts","../src/plugin/messages.ts"],"sourcesContent":["/**\n * Vision Plugin Definition\n *\n * Complete plugin export - NO REACT.\n * This is the main plugin definition that gets registered with pluginRegistry.\n */\n\nimport type { PluginWithHandlers, PluginIssue, IssueContribution } from \"uilint-core\";\nimport { pluginRegistry } from \"uilint-core\";\n\nimport { visionStateDefinition, type VisionState } from \"./state.js\";\nimport { visionActionHandlers } from \"./actions.js\";\nimport { visionCommands } from \"./commands.js\";\nimport { visionToolbarGroups } from \"./toolbar.js\";\nimport { visionPanelDefinitions } from \"./panels.js\";\nimport { visionRuleDefinitions } from \"./rules.js\";\nimport { visionMessageHandlers } from \"./messages.js\";\n\n/**\n * Vision plugin definition\n *\n * Contains everything needed for the vision feature EXCEPT React components.\n * The host (uilint-react) imports this and renders the appropriate UI.\n */\nexport const visionPlugin: PluginWithHandlers<VisionState> = {\n // === Metadata ===\n id: \"vision\",\n name: \"Vision Analysis\",\n version: \"1.0.0\",\n description: \"AI-powered visual UI consistency checking\",\n icon: \"eye\",\n\n // === State ===\n state: visionStateDefinition,\n actions: visionActionHandlers,\n\n // === UI Contributions (Declarative) ===\n commands: visionCommands,\n toolbarGroups: visionToolbarGroups,\n panels: visionPanelDefinitions,\n\n // === Rules ===\n rules: visionRuleDefinitions,\n handlesRuleCategories: [\"vision\"],\n\n // === WebSocket ===\n messageHandlers: visionMessageHandlers,\n\n // === Issue Aggregation ===\n getIssues: (state: VisionState): IssueContribution => {\n const issues = new Map<string, PluginIssue[]>();\n\n for (const [route, visionIssues] of state.visionIssuesCache) {\n for (const issue of visionIssues) {\n if (issue.dataLoc) {\n const existing = issues.get(issue.dataLoc) || [];\n issues.set(issue.dataLoc, [\n ...existing,\n {\n id: `vision:${route}:${issue.elementText}:${Date.now()}`,\n message: issue.message,\n severity: issue.severity,\n ruleId: \"semantic-vision\",\n category: issue.category,\n data: {\n route,\n elementText: issue.elementText,\n suggestion: issue.suggestion,\n },\n },\n ]);\n }\n }\n }\n\n return { pluginId: \"vision\", issues };\n },\n\n // === Browser Actions ===\n browserActions: [\"capture-screenshot\", \"collect-manifest\"],\n\n // === Lifecycle ===\n onLoad: (ctx) => {\n // Check vision availability on load if connected\n if (ctx.websocket.isConnected) {\n ctx.websocket.send({ type: \"vision:check\" });\n }\n },\n};\n\n// Auto-register with plugin registry on import\npluginRegistry.register(visionPlugin);\n\nexport default visionPlugin;\n","/**\n * Vision Plugin Types\n *\n * All types for vision-based UI consistency analysis.\n * Consolidated from uilint-core and uilint-react.\n */\n\n// =============================================================================\n// CORE TYPES\n// =============================================================================\n\n/**\n * Vision issue category\n */\nexport type VisionIssueCategory =\n | \"spacing\"\n | \"alignment\"\n | \"color\"\n | \"typography\"\n | \"layout\"\n | \"contrast\"\n | \"visual-hierarchy\"\n | \"other\";\n\n/**\n * Vision issue severity\n */\nexport type VisionIssueSeverity = \"error\" | \"warning\" | \"info\";\n\n/**\n * Vision analysis issue from the LLM\n */\nexport interface VisionIssue {\n /** Text of the element this issue refers to */\n elementText: string;\n /** Issue description */\n message: string;\n /** Issue category */\n category: VisionIssueCategory;\n /** Severity level */\n severity: VisionIssueSeverity;\n /** Matched dataLoc from manifest (filled in after text matching) */\n dataLoc?: string;\n /** Matched element ID (filled in after text matching) */\n elementId?: string;\n /** Suggested fix (optional) */\n suggestion?: string;\n}\n\n/**\n * Element manifest entry for vision analysis.\n * Maps visible elements to their source locations.\n */\nexport interface ElementManifest {\n /** Unique ID (data-loc if present, otherwise generated) */\n id: string;\n /** Visible text content (truncated to 100 chars) */\n text: string;\n /** data-loc value: \"path:line:column\" */\n dataLoc: string;\n /** Bounding rectangle */\n rect: { x: number; y: number; width: number; height: number };\n /** HTML tag name */\n tagName: string;\n /** Inferred semantic role (button, heading, link, etc.) */\n role?: string;\n /** Total instances with same dataLoc (if deduplicated) */\n instanceCount?: number;\n}\n\n// =============================================================================\n// CAPTURE TYPES\n// =============================================================================\n\n/**\n * Region bounds for partial screenshot capture\n */\nexport interface CaptureRegion {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Capture mode for vision analysis\n */\nexport type CaptureMode = \"full\" | \"region\";\n\n/**\n * Screenshot capture entry for the gallery\n */\nexport interface ScreenshotCapture {\n /** Unique ID for this capture */\n id: string;\n /** Route where the capture was taken */\n route: string;\n /** Base64 data URL of the screenshot (for in-memory captures) */\n dataUrl?: string;\n /** Filename for persisted screenshots (used to fetch from API) */\n filename?: string;\n /** Unix timestamp when captured */\n timestamp: number;\n /** Type of capture */\n type: CaptureMode;\n /** Region bounds if type is 'region' */\n region?: CaptureRegion;\n /** Whether this is a persisted screenshot loaded from disk */\n persisted?: boolean;\n /** Vision issues specific to this capture */\n issues?: VisionIssue[];\n}\n\n// =============================================================================\n// ANALYSIS TYPES\n// =============================================================================\n\n/**\n * Vision analysis result from the analyzer\n */\nexport interface VisionAnalysisResult {\n /** Route/path that was analyzed */\n route: string;\n /** Timestamp of capture */\n timestamp: number;\n /** Screenshot as base64 data URL */\n screenshotDataUrl?: string;\n /** Element manifest */\n manifest: ElementManifest[];\n /** Issues found by vision analysis */\n issues: VisionIssue[];\n /** Analysis duration in ms */\n analysisTime: number;\n /** Error message if analysis failed */\n error?: string;\n /** Full prompt sent to the model (for debugging) */\n prompt?: string;\n /** Raw LLM response (for debugging) */\n rawResponse?: string;\n}\n\n// =============================================================================\n// SETTINGS & STATE TYPES\n// =============================================================================\n\n/**\n * Auto-scan settings for Vision analysis.\n * Persisted to localStorage.\n */\nexport interface VisionAutoScanSettings {\n /** Auto-capture and analyze on route change */\n onRouteChange: boolean;\n /** Auto-capture and analyze on initial page load */\n onInitialLoad: boolean;\n}\n\n/**\n * Default vision auto-scan settings\n */\nexport const DEFAULT_VISION_AUTO_SCAN_SETTINGS: VisionAutoScanSettings = {\n onRouteChange: false,\n onInitialLoad: false,\n};\n\n/**\n * Vision pipeline stage (for error tracking)\n */\nexport type VisionStage = \"capture\" | \"manifest\" | \"ws\" | \"vision\";\n\n/**\n * Vision error information with stage context\n */\nexport interface VisionErrorInfo {\n /** The stage where the error occurred */\n stage: VisionStage;\n /** Human-readable error message */\n message: string;\n /** Route that was being analyzed */\n route: string;\n /** Timestamp of the error */\n timestamp: number;\n}\n\n// =============================================================================\n// PERSISTENCE TYPES\n// =============================================================================\n\n/**\n * Persisted screenshot metadata from the API\n */\nexport interface PersistedScreenshotMetadata {\n filename: string;\n timestamp: number;\n screenshotFile: string;\n route: string | null;\n issues: VisionIssue[] | null;\n manifest: ElementManifest[] | null;\n analysisResult: {\n route: string;\n timestamp: number;\n issues: VisionIssue[];\n analysisTime: number;\n error?: string;\n } | null;\n}\n\n/**\n * API response for listing screenshots\n */\nexport interface ScreenshotListResponse {\n screenshots: Array<{\n filename: string;\n metadata: PersistedScreenshotMetadata | null;\n }>;\n projectRoot: string;\n screenshotsDir: string;\n}\n\n// =============================================================================\n// WEBSOCKET MESSAGE TYPES\n// =============================================================================\n\n/**\n * Client -> Server: Request vision analysis\n */\nexport interface VisionAnalyzeMessage {\n type: \"vision:analyze\";\n route: string;\n timestamp: number;\n screenshot: string;\n manifest: ElementManifest[];\n requestId?: string;\n}\n\n/**\n * Server -> Client: Vision analysis result\n */\nexport interface VisionResultMessage {\n type: \"vision:result\";\n route: string;\n issues: VisionIssue[];\n analysisTime: number;\n error?: string;\n requestId?: string;\n}\n\n/**\n * Server -> Client: Vision analysis progress\n */\nexport interface VisionProgressMessage {\n type: \"vision:progress\";\n route: string;\n phase: string;\n requestId?: string;\n}\n\n/**\n * Client -> Server: Check vision availability\n */\nexport interface VisionCheckMessage {\n type: \"vision:check\";\n requestId?: string;\n}\n\n/**\n * Server -> Client: Vision availability status\n */\nexport interface VisionStatusMessage {\n type: \"vision:status\";\n available: boolean;\n model?: string;\n requestId?: string;\n}\n\n/**\n * Union of all vision WebSocket messages\n */\nexport type VisionMessage =\n | VisionAnalyzeMessage\n | VisionResultMessage\n | VisionProgressMessage\n | VisionCheckMessage\n | VisionStatusMessage;\n","/**\n * Vision Plugin State\n *\n * State shape and initial state for the vision plugin.\n * No React - pure TypeScript.\n */\n\nimport type { StateDefinition } from \"uilint-core\";\nimport type {\n ScreenshotCapture,\n VisionIssue,\n VisionAutoScanSettings,\n VisionErrorInfo,\n CaptureMode,\n CaptureRegion,\n} from \"../types.js\";\nimport { DEFAULT_VISION_AUTO_SCAN_SETTINGS } from \"../types.js\";\n\n/**\n * Vision plugin state shape\n */\nexport interface VisionState {\n // === Availability ===\n /** Whether vision analysis is available (Ollama running with vision model) */\n visionAvailable: boolean;\n /** The vision model being used */\n visionModel: string | null;\n\n // === Analysis State ===\n /** Whether vision analysis is in progress */\n visionAnalyzing: boolean;\n /** Current phase of analysis (capture, manifest, analyzing, etc.) */\n visionProgressPhase: string | null;\n\n // === Capture State ===\n /** Current capture mode */\n captureMode: CaptureMode;\n /** Whether region selection is active */\n regionSelectionActive: boolean;\n /** Selected region bounds (if in region mode) */\n selectedRegion: CaptureRegion | null;\n\n // === Results ===\n /** Screenshot history keyed by ID */\n screenshotHistory: Map<string, ScreenshotCapture>;\n /** Vision issues keyed by route */\n visionIssuesCache: Map<string, VisionIssue[]>;\n\n // === Error State ===\n /** Last error that occurred */\n lastError: VisionErrorInfo | null;\n\n // === Settings ===\n /** Auto-scan settings */\n autoScanSettings: VisionAutoScanSettings;\n}\n\n/**\n * Initial state for the vision plugin\n */\nexport const visionInitialState: VisionState = {\n // Availability\n visionAvailable: false,\n visionModel: null,\n\n // Analysis state\n visionAnalyzing: false,\n visionProgressPhase: null,\n\n // Capture state\n captureMode: \"full\",\n regionSelectionActive: false,\n selectedRegion: null,\n\n // Results\n screenshotHistory: new Map(),\n visionIssuesCache: new Map(),\n\n // Error state\n lastError: null,\n\n // Settings\n autoScanSettings: DEFAULT_VISION_AUTO_SCAN_SETTINGS,\n};\n\n/**\n * State definition for the plugin system\n */\nexport const visionStateDefinition: StateDefinition<VisionState> = {\n initialState: visionInitialState,\n\n computed: {\n /** Whether there are any screenshots */\n hasScreenshots: (state) => state.screenshotHistory.size > 0,\n\n /** Total number of issues across all routes */\n totalIssues: (state) => {\n let count = 0;\n for (const issues of state.visionIssuesCache.values()) {\n count += issues.length;\n }\n return count;\n },\n\n /** Whether currently capturing */\n isCapturing: (state) =>\n state.visionAnalyzing && state.visionProgressPhase === \"capture\",\n\n /** Whether currently analyzing (after capture) */\n isAnalyzing: (state) =>\n state.visionAnalyzing && state.visionProgressPhase === \"analyzing\",\n\n /** All screenshots as an array (sorted by timestamp, newest first) */\n screenshotList: (state) =>\n Array.from(state.screenshotHistory.values()).sort(\n (a, b) => b.timestamp - a.timestamp\n ),\n },\n\n persist: {\n key: \"uilint-vision\",\n include: [\"autoScanSettings\"],\n },\n};\n","/**\n * Vision Plugin Action Handlers\n *\n * Plain functions that handle plugin actions.\n * No React - uses PluginContext for state management.\n */\n\nimport type { ActionHandlers, PluginContext } from \"uilint-core\";\nimport type { VisionState } from \"./state.js\";\nimport type {\n VisionIssue,\n ScreenshotCapture,\n CaptureRegion,\n VisionAutoScanSettings,\n} from \"../types.js\";\n\n// Helper type for cleaner action handler definitions\ntype Handler<TPayload = void> = (\n ctx: PluginContext<VisionState>,\n payload: TPayload\n) => void | Promise<void>;\n\n// Cast helper for proper typing - wraps typed handlers to satisfy ActionHandlers\nconst h = <TPayload = void>(fn: Handler<TPayload>): Handler<unknown> =>\n fn as Handler<unknown>;\n\n/**\n * Action handlers for the vision plugin\n */\nexport const visionActionHandlers: ActionHandlers<VisionState> = {\n /**\n * Set vision availability status\n */\n \"set-vision-available\": h<{ available: boolean; model?: string }>((ctx, payload) => {\n ctx.setState({\n visionAvailable: payload.available,\n visionModel: payload.model ?? null,\n });\n }),\n\n /**\n * Trigger full page capture\n */\n \"capture-full-page\": h(async (ctx) => {\n ctx.setState({\n captureMode: \"full\",\n regionSelectionActive: false,\n selectedRegion: null,\n });\n await ctx.dispatch(\"trigger-vision-analysis\");\n }),\n\n /**\n * Enter region selection mode\n */\n \"enter-region-selection\": h((ctx) => {\n ctx.setState({\n captureMode: \"region\",\n regionSelectionActive: true,\n selectedRegion: null,\n });\n }),\n\n /**\n * Exit region selection mode\n */\n \"exit-region-selection\": h((ctx) => {\n ctx.setState({\n regionSelectionActive: false,\n selectedRegion: null,\n });\n }),\n\n /**\n * Set selected region and trigger capture\n */\n \"set-selected-region\": h<{ region: CaptureRegion }>(async (ctx, payload) => {\n ctx.setState({\n selectedRegion: payload.region,\n regionSelectionActive: false,\n });\n await ctx.dispatch(\"trigger-vision-analysis\");\n }),\n\n /**\n * Trigger vision analysis\n * Requests browser actions for capture and manifest, then sends to server\n */\n \"trigger-vision-analysis\": h(async (ctx) => {\n const state = ctx.getState();\n\n if (!state.visionAvailable) {\n ctx.setState({\n lastError: {\n stage: \"vision\",\n message: \"Vision analysis not available. Check Ollama connection.\",\n route: ctx.getCurrentRoute(),\n timestamp: Date.now(),\n },\n });\n return;\n }\n\n ctx.setState({\n visionAnalyzing: true,\n visionProgressPhase: \"capture\",\n lastError: null,\n });\n\n try {\n // Request screenshot capture from browser\n const captureResult = await ctx.requestBrowserAction(\"capture-screenshot\", {\n mode: state.captureMode,\n region: state.selectedRegion,\n });\n\n if (!captureResult.success) {\n throw new Error(captureResult.error || \"Failed to capture screenshot\");\n }\n\n ctx.setState({ visionProgressPhase: \"manifest\" });\n\n // Request manifest collection from browser\n const manifestResult = await ctx.requestBrowserAction(\"collect-manifest\", {\n region: state.selectedRegion,\n });\n\n if (!manifestResult.success) {\n throw new Error(manifestResult.error || \"Failed to collect manifest\");\n }\n\n ctx.setState({ visionProgressPhase: \"analyzing\" });\n\n const route = ctx.getCurrentRoute();\n const timestamp = Date.now();\n\n // Add to screenshot history\n const capture: ScreenshotCapture = {\n id: `capture-${timestamp}`,\n route,\n dataUrl: captureResult.dataUrl as string,\n timestamp,\n type: state.captureMode,\n region: state.selectedRegion ?? undefined,\n };\n\n const newHistory = new Map(state.screenshotHistory);\n newHistory.set(capture.id, capture);\n ctx.setState({ screenshotHistory: newHistory });\n\n // Send to server for analysis\n ctx.websocket.send({\n type: \"vision:analyze\",\n route,\n timestamp,\n screenshot: captureResult.dataUrl,\n manifest: manifestResult.elements,\n });\n } catch (error) {\n ctx.setState({\n visionAnalyzing: false,\n visionProgressPhase: null,\n lastError: {\n stage: \"capture\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n route: ctx.getCurrentRoute(),\n timestamp: Date.now(),\n },\n });\n }\n }),\n\n /**\n * Handle vision analysis result from server\n */\n \"handle-vision-result\": h<{\n route: string;\n issues: VisionIssue[];\n analysisTime?: number;\n error?: string;\n }>((ctx, payload) => {\n const state = ctx.getState();\n\n if (payload.error) {\n ctx.setState({\n visionAnalyzing: false,\n visionProgressPhase: null,\n lastError: {\n stage: \"vision\",\n message: payload.error,\n route: payload.route,\n timestamp: Date.now(),\n },\n });\n return;\n }\n\n // Update issues cache\n const newCache = new Map(state.visionIssuesCache);\n newCache.set(payload.route, payload.issues);\n\n // Update the latest capture with issues\n const latestCapture = Array.from(state.screenshotHistory.values())\n .filter((c) => c.route === payload.route)\n .sort((a, b) => b.timestamp - a.timestamp)[0];\n\n if (latestCapture) {\n const newHistory = new Map(state.screenshotHistory);\n newHistory.set(latestCapture.id, { ...latestCapture, issues: payload.issues });\n ctx.setState({ screenshotHistory: newHistory });\n }\n\n ctx.setState({\n visionAnalyzing: false,\n visionProgressPhase: null,\n visionIssuesCache: newCache,\n lastError: null,\n });\n }),\n\n /**\n * Handle vision progress update\n */\n \"handle-vision-progress\": h<{ phase: string }>((ctx, payload) => {\n ctx.setState({ visionProgressPhase: payload.phase });\n }),\n\n /**\n * Clear all screenshots and cached issues\n */\n \"clear-screenshots\": h((ctx) => {\n ctx.setState({\n screenshotHistory: new Map(),\n visionIssuesCache: new Map(),\n });\n }),\n\n /**\n * Delete a specific screenshot\n */\n \"delete-screenshot\": h<{ id: string }>((ctx, payload) => {\n const state = ctx.getState();\n const newHistory = new Map(state.screenshotHistory);\n newHistory.delete(payload.id);\n ctx.setState({ screenshotHistory: newHistory });\n }),\n\n /**\n * Update auto-scan settings\n */\n \"update-auto-scan-settings\": h<Partial<VisionAutoScanSettings>>((ctx, payload) => {\n const state = ctx.getState();\n ctx.setState({\n autoScanSettings: { ...state.autoScanSettings, ...payload },\n });\n }),\n\n /**\n * Focus an issue in the heatmap\n */\n \"focus-heatmap\": h<{ dataLoc: string }>((ctx, payload) => {\n ctx.setHeatmapFilter([payload.dataLoc], \"Vision Issue\");\n }),\n\n /**\n * Clear heatmap filter\n */\n \"clear-heatmap-filter\": h((ctx) => {\n ctx.clearHeatmapFilter();\n }),\n\n /**\n * Open file in editor\n */\n \"open-editor\": h<{ dataLoc: string }>((ctx, payload) => {\n ctx.openInEditor(payload.dataLoc);\n }),\n\n /**\n * Select a capture to view in inspector\n */\n \"select-capture\": h<{ captureId: string }>((ctx, payload) => {\n const state = ctx.getState();\n const capture = state.screenshotHistory.get(payload.captureId);\n if (capture) {\n ctx.openInspector(\"vision-gallery\", { capture });\n }\n }),\n};\n","/**\n * Vision Plugin Commands\n *\n * Command palette commands for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { CommandDefinition } from \"uilint-core\";\n\n/**\n * Vision plugin commands\n */\nexport const visionCommands: CommandDefinition[] = [\n {\n id: \"vision:capture-full-page\",\n title: \"Capture Full Page\",\n keywords: [\"vision\", \"screenshot\", \"capture\", \"full\", \"page\", \"analyze\"],\n category: \"Vision\",\n subtitle: \"Capture and analyze the entire visible page\",\n icon: \"camera\",\n shortcut: \"Cmd+Shift+C\",\n action: { type: \"capture-full-page\" },\n isAvailable: { binding: \"visionAvailable\" },\n },\n {\n id: \"vision:capture-region\",\n title: \"Capture Region\",\n keywords: [\"vision\", \"screenshot\", \"capture\", \"region\", \"area\", \"select\"],\n category: \"Vision\",\n subtitle: \"Select a region of the page to capture and analyze\",\n icon: \"crop\",\n shortcut: \"Cmd+Shift+R\",\n action: { type: \"enter-region-selection\" },\n isAvailable: { binding: \"visionAvailable\" },\n },\n {\n id: \"vision:clear-screenshots\",\n title: \"Clear Screenshots\",\n keywords: [\"vision\", \"clear\", \"delete\", \"screenshots\", \"history\"],\n category: \"Vision\",\n subtitle: \"Clear all captured screenshots and cached results\",\n icon: \"trash\",\n action: { type: \"clear-screenshots\" },\n isAvailable: { expression: \"screenshotHistory.size > 0\" },\n },\n {\n id: \"vision:toggle-auto-scan-route\",\n title: \"Toggle Auto-Scan on Route Change\",\n keywords: [\"vision\", \"auto\", \"scan\", \"route\", \"change\", \"automatic\"],\n category: \"Vision\",\n subtitle: \"Automatically capture and analyze on route change\",\n icon: \"refresh\",\n action: {\n type: \"update-auto-scan-settings\",\n payloadBindings: { onRouteChange: \"!autoScanSettings.onRouteChange\" },\n },\n isAvailable: { binding: \"visionAvailable\" },\n },\n];\n","/**\n * Vision Plugin Toolbar\n *\n * Toolbar action group definitions for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { ToolbarGroupDefinition } from \"uilint-core\";\n\n/**\n * Vision capture toolbar group\n */\nexport const visionToolbarGroup: ToolbarGroupDefinition = {\n id: \"vision:capture\",\n icon: \"camera\",\n tooltip: \"Vision Capture\",\n priority: 100,\n isVisible: { binding: \"visionAvailable\" },\n\n actions: [\n {\n id: \"vision:capture-full\",\n icon: \"camera\",\n tooltip: \"Capture Full Page\",\n action: { type: \"capture-full-page\" },\n },\n {\n id: \"vision:capture-region\",\n icon: \"crop\",\n tooltip: \"Capture Region\",\n action: { type: \"enter-region-selection\" },\n },\n ],\n};\n\n/**\n * All toolbar groups for the vision plugin\n */\nexport const visionToolbarGroups: ToolbarGroupDefinition[] = [visionToolbarGroup];\n","/**\n * Vision Plugin Panels\n *\n * Inspector panel definitions for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { PanelDefinition } from \"uilint-core\";\n\n/**\n * Vision issue inspector panel\n */\nexport const visionIssuePanelDefinition: PanelDefinition = {\n id: \"vision-issue\",\n title: { binding: \"issue.category\" },\n priority: 10,\n\n empty: {\n when: { expression: \"!issue\" },\n message: \"No vision issue selected.\",\n icon: \"eye\",\n },\n\n layout: [\n // Header with message\n {\n type: \"header\",\n icon: \"eye\",\n text: { binding: \"issue.message\" },\n sticky: true,\n },\n\n // Severity badge\n {\n type: \"badge\",\n variant: \"severity\",\n value: { binding: \"issue.severity\" },\n centered: false,\n },\n\n // Category badge\n {\n type: \"badge\",\n variant: \"category\",\n value: { binding: \"issue.category\" },\n centered: false,\n },\n\n // Element text (if available)\n {\n type: \"conditional\",\n condition: { binding: \"issue.elementText\" },\n then: [\n {\n type: \"text\",\n content: { binding: \"issue.elementText\" },\n variant: \"caption\",\n },\n ],\n },\n\n // Suggestion (if available)\n {\n type: \"conditional\",\n condition: { binding: \"issue.suggestion\" },\n then: [\n { type: \"divider\", spacing: \"small\" },\n {\n type: \"header\",\n icon: \"sparkles\",\n text: \"Suggestion\",\n },\n {\n type: \"text\",\n content: { binding: \"issue.suggestion\" },\n variant: \"body\",\n },\n ],\n },\n\n { type: \"divider\", spacing: \"medium\" },\n\n // Actions\n {\n type: \"actions\",\n direction: \"column\",\n actions: [\n {\n id: \"focus-heatmap\",\n label: \"Focus in Heatmap\",\n icon: \"filter\",\n variant: \"secondary\",\n action: {\n type: \"focus-heatmap\",\n payloadBindings: { dataLoc: \"issue.dataLoc\" },\n },\n visible: { binding: \"issue.dataLoc\" },\n },\n {\n id: \"open-editor\",\n label: \"Open in Editor\",\n icon: \"external-link\",\n variant: \"ghost\",\n action: {\n type: \"open-editor\",\n payloadBindings: { dataLoc: \"issue.dataLoc\" },\n },\n visible: { binding: \"issue.dataLoc\" },\n },\n ],\n },\n ],\n};\n\n/**\n * Screenshot gallery panel\n */\nexport const screenshotGalleryPanelDefinition: PanelDefinition = {\n id: \"vision-gallery\",\n title: \"Screenshots\",\n priority: 5,\n\n empty: {\n when: { expression: \"screenshots.length === 0\" },\n message: \"No screenshots captured yet.\",\n submessage: \"Use the capture button to take a screenshot.\",\n icon: \"camera\",\n },\n\n layout: [\n {\n type: \"list\",\n items: { binding: \"screenshots\" },\n itemLayout: [\n {\n type: \"card\",\n thumbnail: { binding: \"item.dataUrl\" },\n title: { binding: \"item.route\" },\n subtitle: { binding: \"item.timestamp\" },\n badge: {\n variant: \"count\",\n value: { binding: \"item.issues.length\" },\n label: \"issues\",\n },\n onClick: {\n type: \"select-capture\",\n payloadBindings: { captureId: \"item.id\" },\n },\n },\n ],\n },\n ],\n};\n\n/**\n * All vision panel definitions\n */\nexport const visionPanelDefinitions: PanelDefinition[] = [\n visionIssuePanelDefinition,\n screenshotGalleryPanelDefinition,\n];\n","/**\n * Vision Plugin Rules\n *\n * Rule definitions for the vision plugin.\n * Declarative - no React.\n */\n\nimport type { RuleDefinition } from \"uilint-core\";\n\n/**\n * semantic-vision rule definition\n */\nexport const semanticVisionRuleDefinition: RuleDefinition = {\n id: \"semantic-vision\",\n name: \"Vision Analysis\",\n description: \"AI-powered visual UI consistency checking\",\n category: \"vision\",\n icon: \"eye\",\n defaultSeverity: \"warn\",\n defaultEnabled: false,\n heatmapColor: \"#8b5cf6\", // Purple\n customInspectorPanel: \"vision-issue\",\n requirements: [\n {\n type: \"ollama\",\n description: \"Requires Ollama with a vision model\",\n setupHint: \"Run: ollama pull qwen3-vl:8b-instruct\",\n },\n {\n type: \"connection\",\n description: \"Requires connection to uilint server\",\n setupHint: \"Run: uilint serve\",\n },\n ],\n docs: `\n## Vision Analysis Rule\n\nThis rule analyzes screenshots of your UI for visual consistency issues using AI vision models.\n\n### Categories of Issues\n\n- **Spacing**: Inconsistent margins, padding, or gaps\n- **Alignment**: Elements not properly aligned\n- **Color**: Color inconsistencies or accessibility issues\n- **Typography**: Font size, weight, or style inconsistencies\n- **Layout**: Layout problems or broken layouts\n- **Contrast**: Insufficient contrast for accessibility\n- **Visual Hierarchy**: Issues with visual importance/hierarchy\n\n### Requirements\n\n1. Ollama must be running with a vision model (qwen3-vl recommended)\n2. The UILint server must be connected\n\n### Usage\n\nCapture screenshots using the Vision toolbar or command palette, and issues will be reported automatically.\n `.trim(),\n};\n\n/**\n * All vision rule definitions\n */\nexport const visionRuleDefinitions: RuleDefinition[] = [semanticVisionRuleDefinition];\n","/**\n * Vision Plugin Message Handlers\n *\n * WebSocket message handlers for the vision plugin.\n * Plain functions - no React.\n */\n\nimport type { MessageHandlers, PluginContext } from \"uilint-core\";\nimport type { VisionState } from \"./state.js\";\nimport type {\n VisionStatusMessage,\n VisionProgressMessage,\n VisionResultMessage,\n} from \"../types.js\";\n\n/**\n * Message handlers for the vision plugin\n */\nexport const visionMessageHandlers: MessageHandlers<VisionState> = {\n /**\n * Handle vision availability status\n */\n \"vision:status\": (\n ctx: PluginContext<VisionState>,\n message: unknown\n ) => {\n const msg = message as VisionStatusMessage;\n ctx.dispatch(\"set-vision-available\", {\n available: msg.available,\n model: msg.model,\n });\n },\n\n /**\n * Handle vision analysis progress\n */\n \"vision:progress\": (\n ctx: PluginContext<VisionState>,\n message: unknown\n ) => {\n const msg = message as VisionProgressMessage;\n ctx.dispatch(\"handle-vision-progress\", { phase: msg.phase });\n },\n\n /**\n * Handle vision analysis result\n */\n \"vision:result\": (\n ctx: PluginContext<VisionState>,\n message: unknown\n ) => {\n const msg = message as VisionResultMessage;\n ctx.dispatch(\"handle-vision-result\", {\n route: msg.route,\n issues: msg.issues,\n analysisTime: msg.analysisTime,\n error: msg.error,\n });\n },\n};\n"],"mappings":";AAQA,SAAS,sBAAsB;;;ACuJxB,IAAM,oCAA4D;AAAA,EACvE,eAAe;AAAA,EACf,eAAe;AACjB;;;ACtGO,IAAM,qBAAkC;AAAA;AAAA,EAE7C,iBAAiB;AAAA,EACjB,aAAa;AAAA;AAAA,EAGb,iBAAiB;AAAA,EACjB,qBAAqB;AAAA;AAAA,EAGrB,aAAa;AAAA,EACb,uBAAuB;AAAA,EACvB,gBAAgB;AAAA;AAAA,EAGhB,mBAAmB,oBAAI,IAAI;AAAA,EAC3B,mBAAmB,oBAAI,IAAI;AAAA;AAAA,EAG3B,WAAW;AAAA;AAAA,EAGX,kBAAkB;AACpB;AAKO,IAAM,wBAAsD;AAAA,EACjE,cAAc;AAAA,EAEd,UAAU;AAAA;AAAA,IAER,gBAAgB,CAAC,UAAU,MAAM,kBAAkB,OAAO;AAAA;AAAA,IAG1D,aAAa,CAAC,UAAU;AACtB,UAAI,QAAQ;AACZ,iBAAW,UAAU,MAAM,kBAAkB,OAAO,GAAG;AACrD,iBAAS,OAAO;AAAA,MAClB;AACA,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,aAAa,CAAC,UACZ,MAAM,mBAAmB,MAAM,wBAAwB;AAAA;AAAA,IAGzD,aAAa,CAAC,UACZ,MAAM,mBAAmB,MAAM,wBAAwB;AAAA;AAAA,IAGzD,gBAAgB,CAAC,UACf,MAAM,KAAK,MAAM,kBAAkB,OAAO,CAAC,EAAE;AAAA,MAC3C,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE;AAAA,IAC5B;AAAA,EACJ;AAAA,EAEA,SAAS;AAAA,IACP,KAAK;AAAA,IACL,SAAS,CAAC,kBAAkB;AAAA,EAC9B;AACF;;;ACpGA,IAAM,IAAI,CAAkB,OAC1B;AAKK,IAAM,uBAAoD;AAAA;AAAA;AAAA;AAAA,EAI/D,wBAAwB,EAA0C,CAAC,KAAK,YAAY;AAClF,QAAI,SAAS;AAAA,MACX,iBAAiB,QAAQ;AAAA,MACzB,aAAa,QAAQ,SAAS;AAAA,IAChC,CAAC;AAAA,EACH,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,qBAAqB,EAAE,OAAO,QAAQ;AACpC,QAAI,SAAS;AAAA,MACX,aAAa;AAAA,MACb,uBAAuB;AAAA,MACvB,gBAAgB;AAAA,IAClB,CAAC;AACD,UAAM,IAAI,SAAS,yBAAyB;AAAA,EAC9C,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,0BAA0B,EAAE,CAAC,QAAQ;AACnC,QAAI,SAAS;AAAA,MACX,aAAa;AAAA,MACb,uBAAuB;AAAA,MACvB,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,yBAAyB,EAAE,CAAC,QAAQ;AAClC,QAAI,SAAS;AAAA,MACX,uBAAuB;AAAA,MACvB,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,uBAAuB,EAA6B,OAAO,KAAK,YAAY;AAC1E,QAAI,SAAS;AAAA,MACX,gBAAgB,QAAQ;AAAA,MACxB,uBAAuB;AAAA,IACzB,CAAC;AACD,UAAM,IAAI,SAAS,yBAAyB;AAAA,EAC9C,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMD,2BAA2B,EAAE,OAAO,QAAQ;AAC1C,UAAM,QAAQ,IAAI,SAAS;AAE3B,QAAI,CAAC,MAAM,iBAAiB;AAC1B,UAAI,SAAS;AAAA,QACX,WAAW;AAAA,UACT,OAAO;AAAA,UACP,SAAS;AAAA,UACT,OAAO,IAAI,gBAAgB;AAAA,UAC3B,WAAW,KAAK,IAAI;AAAA,QACtB;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,QAAI,SAAS;AAAA,MACX,iBAAiB;AAAA,MACjB,qBAAqB;AAAA,MACrB,WAAW;AAAA,IACb,CAAC;AAED,QAAI;AAEF,YAAM,gBAAgB,MAAM,IAAI,qBAAqB,sBAAsB;AAAA,QACzE,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM;AAAA,MAChB,CAAC;AAED,UAAI,CAAC,cAAc,SAAS;AAC1B,cAAM,IAAI,MAAM,cAAc,SAAS,8BAA8B;AAAA,MACvE;AAEA,UAAI,SAAS,EAAE,qBAAqB,WAAW,CAAC;AAGhD,YAAM,iBAAiB,MAAM,IAAI,qBAAqB,oBAAoB;AAAA,QACxE,QAAQ,MAAM;AAAA,MAChB,CAAC;AAED,UAAI,CAAC,eAAe,SAAS;AAC3B,cAAM,IAAI,MAAM,eAAe,SAAS,4BAA4B;AAAA,MACtE;AAEA,UAAI,SAAS,EAAE,qBAAqB,YAAY,CAAC;AAEjD,YAAM,QAAQ,IAAI,gBAAgB;AAClC,YAAM,YAAY,KAAK,IAAI;AAG3B,YAAM,UAA6B;AAAA,QACjC,IAAI,WAAW,SAAS;AAAA,QACxB;AAAA,QACA,SAAS,cAAc;AAAA,QACvB;AAAA,QACA,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM,kBAAkB;AAAA,MAClC;AAEA,YAAM,aAAa,IAAI,IAAI,MAAM,iBAAiB;AAClD,iBAAW,IAAI,QAAQ,IAAI,OAAO;AAClC,UAAI,SAAS,EAAE,mBAAmB,WAAW,CAAC;AAG9C,UAAI,UAAU,KAAK;AAAA,QACjB,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,YAAY,cAAc;AAAA,QAC1B,UAAU,eAAe;AAAA,MAC3B,CAAC;AAAA,IACH,SAAS,OAAO;AACd,UAAI,SAAS;AAAA,QACX,iBAAiB;AAAA,QACjB,qBAAqB;AAAA,QACrB,WAAW;AAAA,UACT,OAAO;AAAA,UACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,UAClD,OAAO,IAAI,gBAAgB;AAAA,UAC3B,WAAW,KAAK,IAAI;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,wBAAwB,EAKrB,CAAC,KAAK,YAAY;AACnB,UAAM,QAAQ,IAAI,SAAS;AAE3B,QAAI,QAAQ,OAAO;AACjB,UAAI,SAAS;AAAA,QACX,iBAAiB;AAAA,QACjB,qBAAqB;AAAA,QACrB,WAAW;AAAA,UACT,OAAO;AAAA,UACP,SAAS,QAAQ;AAAA,UACjB,OAAO,QAAQ;AAAA,UACf,WAAW,KAAK,IAAI;AAAA,QACtB;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAGA,UAAM,WAAW,IAAI,IAAI,MAAM,iBAAiB;AAChD,aAAS,IAAI,QAAQ,OAAO,QAAQ,MAAM;AAG1C,UAAM,gBAAgB,MAAM,KAAK,MAAM,kBAAkB,OAAO,CAAC,EAC9D,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ,KAAK,EACvC,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;AAE9C,QAAI,eAAe;AACjB,YAAM,aAAa,IAAI,IAAI,MAAM,iBAAiB;AAClD,iBAAW,IAAI,cAAc,IAAI,EAAE,GAAG,eAAe,QAAQ,QAAQ,OAAO,CAAC;AAC7E,UAAI,SAAS,EAAE,mBAAmB,WAAW,CAAC;AAAA,IAChD;AAEA,QAAI,SAAS;AAAA,MACX,iBAAiB;AAAA,MACjB,qBAAqB;AAAA,MACrB,mBAAmB;AAAA,MACnB,WAAW;AAAA,IACb,CAAC;AAAA,EACH,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,0BAA0B,EAAqB,CAAC,KAAK,YAAY;AAC/D,QAAI,SAAS,EAAE,qBAAqB,QAAQ,MAAM,CAAC;AAAA,EACrD,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,qBAAqB,EAAE,CAAC,QAAQ;AAC9B,QAAI,SAAS;AAAA,MACX,mBAAmB,oBAAI,IAAI;AAAA,MAC3B,mBAAmB,oBAAI,IAAI;AAAA,IAC7B,CAAC;AAAA,EACH,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,qBAAqB,EAAkB,CAAC,KAAK,YAAY;AACvD,UAAM,QAAQ,IAAI,SAAS;AAC3B,UAAM,aAAa,IAAI,IAAI,MAAM,iBAAiB;AAClD,eAAW,OAAO,QAAQ,EAAE;AAC5B,QAAI,SAAS,EAAE,mBAAmB,WAAW,CAAC;AAAA,EAChD,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,6BAA6B,EAAmC,CAAC,KAAK,YAAY;AAChF,UAAM,QAAQ,IAAI,SAAS;AAC3B,QAAI,SAAS;AAAA,MACX,kBAAkB,EAAE,GAAG,MAAM,kBAAkB,GAAG,QAAQ;AAAA,IAC5D,CAAC;AAAA,EACH,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,iBAAiB,EAAuB,CAAC,KAAK,YAAY;AACxD,QAAI,iBAAiB,CAAC,QAAQ,OAAO,GAAG,cAAc;AAAA,EACxD,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,wBAAwB,EAAE,CAAC,QAAQ;AACjC,QAAI,mBAAmB;AAAA,EACzB,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,eAAe,EAAuB,CAAC,KAAK,YAAY;AACtD,QAAI,aAAa,QAAQ,OAAO;AAAA,EAClC,CAAC;AAAA;AAAA;AAAA;AAAA,EAKD,kBAAkB,EAAyB,CAAC,KAAK,YAAY;AAC3D,UAAM,QAAQ,IAAI,SAAS;AAC3B,UAAM,UAAU,MAAM,kBAAkB,IAAI,QAAQ,SAAS;AAC7D,QAAI,SAAS;AACX,UAAI,cAAc,kBAAkB,EAAE,QAAQ,CAAC;AAAA,IACjD;AAAA,EACF,CAAC;AACH;;;ACpRO,IAAM,iBAAsC;AAAA,EACjD;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,CAAC,UAAU,cAAc,WAAW,QAAQ,QAAQ,SAAS;AAAA,IACvE,UAAU;AAAA,IACV,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,EAAE,MAAM,oBAAoB;AAAA,IACpC,aAAa,EAAE,SAAS,kBAAkB;AAAA,EAC5C;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,CAAC,UAAU,cAAc,WAAW,UAAU,QAAQ,QAAQ;AAAA,IACxE,UAAU;AAAA,IACV,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,EAAE,MAAM,yBAAyB;AAAA,IACzC,aAAa,EAAE,SAAS,kBAAkB;AAAA,EAC5C;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,CAAC,UAAU,SAAS,UAAU,eAAe,SAAS;AAAA,IAChE,UAAU;AAAA,IACV,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,EAAE,MAAM,oBAAoB;AAAA,IACpC,aAAa,EAAE,YAAY,6BAA6B;AAAA,EAC1D;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,CAAC,UAAU,QAAQ,QAAQ,SAAS,UAAU,WAAW;AAAA,IACnE,UAAU;AAAA,IACV,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,iBAAiB,EAAE,eAAe,kCAAkC;AAAA,IACtE;AAAA,IACA,aAAa,EAAE,SAAS,kBAAkB;AAAA,EAC5C;AACF;;;AC9CO,IAAM,qBAA6C;AAAA,EACxD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,WAAW,EAAE,SAAS,kBAAkB;AAAA,EAExC,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,QAAQ,EAAE,MAAM,oBAAoB;AAAA,IACtC;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,QAAQ,EAAE,MAAM,yBAAyB;AAAA,IAC3C;AAAA,EACF;AACF;AAKO,IAAM,sBAAgD,CAAC,kBAAkB;;;AC1BzE,IAAM,6BAA8C;AAAA,EACzD,IAAI;AAAA,EACJ,OAAO,EAAE,SAAS,iBAAiB;AAAA,EACnC,UAAU;AAAA,EAEV,OAAO;AAAA,IACL,MAAM,EAAE,YAAY,SAAS;AAAA,IAC7B,SAAS;AAAA,IACT,MAAM;AAAA,EACR;AAAA,EAEA,QAAQ;AAAA;AAAA,IAEN;AAAA,MACE,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM,EAAE,SAAS,gBAAgB;AAAA,MACjC,QAAQ;AAAA,IACV;AAAA;AAAA,IAGA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,MACT,OAAO,EAAE,SAAS,iBAAiB;AAAA,MACnC,UAAU;AAAA,IACZ;AAAA;AAAA,IAGA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,MACT,OAAO,EAAE,SAAS,iBAAiB;AAAA,MACnC,UAAU;AAAA,IACZ;AAAA;AAAA,IAGA;AAAA,MACE,MAAM;AAAA,MACN,WAAW,EAAE,SAAS,oBAAoB;AAAA,MAC1C,MAAM;AAAA,QACJ;AAAA,UACE,MAAM;AAAA,UACN,SAAS,EAAE,SAAS,oBAAoB;AAAA,UACxC,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,MAAM;AAAA,MACN,WAAW,EAAE,SAAS,mBAAmB;AAAA,MACzC,MAAM;AAAA,QACJ,EAAE,MAAM,WAAW,SAAS,QAAQ;AAAA,QACpC;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,QACR;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS,EAAE,SAAS,mBAAmB;AAAA,UACvC,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,IAEA,EAAE,MAAM,WAAW,SAAS,SAAS;AAAA;AAAA,IAGrC;AAAA,MACE,MAAM;AAAA,MACN,WAAW;AAAA,MACX,SAAS;AAAA,QACP;AAAA,UACE,IAAI;AAAA,UACJ,OAAO;AAAA,UACP,MAAM;AAAA,UACN,SAAS;AAAA,UACT,QAAQ;AAAA,YACN,MAAM;AAAA,YACN,iBAAiB,EAAE,SAAS,gBAAgB;AAAA,UAC9C;AAAA,UACA,SAAS,EAAE,SAAS,gBAAgB;AAAA,QACtC;AAAA,QACA;AAAA,UACE,IAAI;AAAA,UACJ,OAAO;AAAA,UACP,MAAM;AAAA,UACN,SAAS;AAAA,UACT,QAAQ;AAAA,YACN,MAAM;AAAA,YACN,iBAAiB,EAAE,SAAS,gBAAgB;AAAA,UAC9C;AAAA,UACA,SAAS,EAAE,SAAS,gBAAgB;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAKO,IAAM,mCAAoD;AAAA,EAC/D,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,UAAU;AAAA,EAEV,OAAO;AAAA,IACL,MAAM,EAAE,YAAY,2BAA2B;AAAA,IAC/C,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,MAAM;AAAA,EACR;AAAA,EAEA,QAAQ;AAAA,IACN;AAAA,MACE,MAAM;AAAA,MACN,OAAO,EAAE,SAAS,cAAc;AAAA,MAChC,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,WAAW,EAAE,SAAS,eAAe;AAAA,UACrC,OAAO,EAAE,SAAS,aAAa;AAAA,UAC/B,UAAU,EAAE,SAAS,iBAAiB;AAAA,UACtC,OAAO;AAAA,YACL,SAAS;AAAA,YACT,OAAO,EAAE,SAAS,qBAAqB;AAAA,YACvC,OAAO;AAAA,UACT;AAAA,UACA,SAAS;AAAA,YACP,MAAM;AAAA,YACN,iBAAiB,EAAE,WAAW,UAAU;AAAA,UAC1C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAKO,IAAM,yBAA4C;AAAA,EACvD;AAAA,EACA;AACF;;;ACpJO,IAAM,+BAA+C;AAAA,EAC1D,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,cAAc;AAAA;AAAA,EACd,sBAAsB;AAAA,EACtB,cAAc;AAAA,IACZ;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW;AAAA,IACb;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAuBJ,KAAK;AACT;AAKO,IAAM,wBAA0C,CAAC,4BAA4B;;;AC7C7E,IAAM,wBAAsD;AAAA;AAAA;AAAA;AAAA,EAIjE,iBAAiB,CACf,KACA,YACG;AACH,UAAM,MAAM;AACZ,QAAI,SAAS,wBAAwB;AAAA,MACnC,WAAW,IAAI;AAAA,MACf,OAAO,IAAI;AAAA,IACb,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,CACjB,KACA,YACG;AACH,UAAM,MAAM;AACZ,QAAI,SAAS,0BAA0B,EAAE,OAAO,IAAI,MAAM,CAAC;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,CACf,KACA,YACG;AACH,UAAM,MAAM;AACZ,QAAI,SAAS,wBAAwB;AAAA,MACnC,OAAO,IAAI;AAAA,MACX,QAAQ,IAAI;AAAA,MACZ,cAAc,IAAI;AAAA,MAClB,OAAO,IAAI;AAAA,IACb,CAAC;AAAA,EACH;AACF;;;ARnCO,IAAM,eAAgD;AAAA;AAAA,EAE3D,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM;AAAA;AAAA,EAGN,OAAO;AAAA,EACP,SAAS;AAAA;AAAA,EAGT,UAAU;AAAA,EACV,eAAe;AAAA,EACf,QAAQ;AAAA;AAAA,EAGR,OAAO;AAAA,EACP,uBAAuB,CAAC,QAAQ;AAAA;AAAA,EAGhC,iBAAiB;AAAA;AAAA,EAGjB,WAAW,CAAC,UAA0C;AACpD,UAAM,SAAS,oBAAI,IAA2B;AAE9C,eAAW,CAAC,OAAO,YAAY,KAAK,MAAM,mBAAmB;AAC3D,iBAAW,SAAS,cAAc;AAChC,YAAI,MAAM,SAAS;AACjB,gBAAM,WAAW,OAAO,IAAI,MAAM,OAAO,KAAK,CAAC;AAC/C,iBAAO,IAAI,MAAM,SAAS;AAAA,YACxB,GAAG;AAAA,YACH;AAAA,cACE,IAAI,UAAU,KAAK,IAAI,MAAM,WAAW,IAAI,KAAK,IAAI,CAAC;AAAA,cACtD,SAAS,MAAM;AAAA,cACf,UAAU,MAAM;AAAA,cAChB,QAAQ;AAAA,cACR,UAAU,MAAM;AAAA,cAChB,MAAM;AAAA,gBACJ;AAAA,gBACA,aAAa,MAAM;AAAA,gBACnB,YAAY,MAAM;AAAA,cACpB;AAAA,YACF;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,UAAU,UAAU,OAAO;AAAA,EACtC;AAAA;AAAA,EAGA,gBAAgB,CAAC,sBAAsB,kBAAkB;AAAA;AAAA,EAGzD,QAAQ,CAAC,QAAQ;AAEf,QAAI,IAAI,UAAU,aAAa;AAC7B,UAAI,UAAU,KAAK,EAAE,MAAM,eAAe,CAAC;AAAA,IAC7C;AAAA,EACF;AACF;AAGA,eAAe,SAAS,YAAY;AAEpC,IAAO,iBAAQ;","names":[]}