From d2f77abca86eddf84f9589bb4cff420014fdf1d4 Mon Sep 17 00:00:00 2001 From: jlukic Date: Sun, 22 Jun 2025 11:33:36 -0400 Subject: [PATCH 01/12] Bug: Fix setting() cannot be used as getter --- packages/query/src/query.js | 47 +++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/query/src/query.js b/packages/query/src/query.js index 7c8be9756..b68a375c0 100755 --- a/packages/query/src/query.js +++ b/packages/query/src/query.js @@ -46,7 +46,7 @@ export class Query { } // this is an existing query object - if(selector instanceof Query) { + if (selector instanceof Query) { elements = selector; } @@ -99,7 +99,7 @@ export class Query { ? new Query(globalThis, this.options) : new Query(elements, { ...this.options, prevObject: this }); } - + end() { return this.prevObject || this; } @@ -116,8 +116,10 @@ export class Query { // Add root if required if (includeRoot) { - if ((domSelector && root == selector) || - (!domSelector && root.matches && root.matches(selector))) { + if ( + (domSelector && root == selector) + || (!domSelector && root.matches && root.matches(selector)) + ) { elements.add(root); } } @@ -125,7 +127,8 @@ export class Query { // Query from root if (domSelector) { queriedRoot = true; - } else if (root.querySelectorAll) { + } + else if (root.querySelectorAll) { root.querySelectorAll(selector).forEach(el => elements.add(el)); queriedRoot = true; } @@ -150,7 +153,8 @@ export class Query { elements.add(selector); domFound = true; } - } else if (node.querySelectorAll) { + } + else if (node.querySelectorAll) { // Directly add to Set without intermediate array node.querySelectorAll(selector).forEach(el => elements.add(el)); } @@ -158,7 +162,7 @@ export class Query { const findElements = (node, selector, query) => { // Early termination condition for DOM selector search - if (domSelector && domFound) return; + if (domSelector && domFound) { return; } // If root element didn't support querySelectorAll, query each child node if (query === true) { @@ -605,10 +609,10 @@ export class Query { getSlot(name) { return this.map((el) => { - if(el.tagName.toLowerCase() == 'slot' && (!name || el.name == name)) { + if (el.tagName.toLowerCase() == 'slot' && (!name || el.name == name)) { // called directly on a matching slot const nodes = el.assignedNodes({ flatten: true }); - if(nodes) { + if (nodes) { return this.chain(nodes).html(); } } @@ -617,10 +621,11 @@ export class Query { const slotSelector = name ? `slot[name="${name}"]` : 'slot:not([name])'; const slot = el.shadowRoot.querySelector(slotSelector); const nodes = slot.assignedNodes({ flatten: true }); - if(nodes) { + if (nodes) { return this.chain(nodes).html(); } - } else { + } + else { // No shadow DOM, fallback to direct DOM querying const slotSelector = name ? `[slot="${name}"]` : ':not([slot])'; return this.chain(el).find(slotSelector).html(); @@ -629,18 +634,18 @@ export class Query { } setSlot(nameOrHTML, newHTML) { - // Determine if we're dealing with a named slot or default slot based on arguments let name; if (newHTML) { name = nameOrHTML; - } else { + } + else { newHTML = nameOrHTML; } return this.each((el) => { // find host web component - if(el.tagName.toLowerCase() == 'slot') { + if (el.tagName.toLowerCase() == 'slot') { el = el.getRootNode().getRootNode()?.host; } const $el = this.chain(el); @@ -653,7 +658,8 @@ export class Query { $slottedElement = this.chain(el).find(slotSelector); } $slottedElement.html(newHTML); - } else { + } + else { // Default slot updates the entire element content $el.html(newHTML); } @@ -717,8 +723,7 @@ export class Query { || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement // web components may store value - || customElements.get(el.tagName.toLowerCase()) - ; + || customElements.get(el.tagName.toLowerCase()); }; if (newValue !== undefined) { // Set the value for each element @@ -795,7 +800,7 @@ export class Query { return this.css(property, null, { includeComputed: true }); } - cssVar(variable, value) { + cssVar(variable, value = null) { return this.css(`--${variable}`, value, { includeComputed: true }); } @@ -1084,6 +1089,12 @@ export class Query { } setting(setting, value) { + if (value === undefined) { + const settings = this.map(el => el[setting]); + return (settings.length == 1) + ? settings[0] + : settings; + } return this.each((el) => { el[setting] = value; }); From fd25f6eb7307600b7299cf39345472a2b9736a23 Mon Sep 17 00:00:00 2001 From: jlukic Date: Sun, 22 Jun 2025 21:55:39 -0400 Subject: [PATCH 02/12] AI: Work on mcp workspace --- ai/workspace/.esbuild/index.js | 114 ++++ ai/workspace/.esbuild/index.js.map | 7 + ai/workspace/README.md | 384 +++++++++++ ai/workspace/components/example/component.css | 182 ++++++ .../components/example/component.html | 52 ++ ai/workspace/components/example/component.js | 83 +++ .../components/test-component/component.js | 599 +++++++++++++++++ ai/workspace/debug-bridge.js | 604 ++++++++++++++++++ ai/workspace/mcp-tools.js | 375 +++++++++++ ai/workspace/public/component.html | 447 +++++++++++++ ai/workspace/public/index.html | 384 +++++++++++ .../scripts/src/build-ai-workspace.js | 184 ++++++ internal-packages/scripts/src/index.js | 3 +- package-lock.json | 7 +- package.json | 4 +- 15 files changed, 3425 insertions(+), 4 deletions(-) create mode 100644 ai/workspace/.esbuild/index.js create mode 100644 ai/workspace/.esbuild/index.js.map create mode 100644 ai/workspace/README.md create mode 100644 ai/workspace/components/example/component.css create mode 100644 ai/workspace/components/example/component.html create mode 100644 ai/workspace/components/example/component.js create mode 100644 ai/workspace/components/test-component/component.js create mode 100644 ai/workspace/debug-bridge.js create mode 100644 ai/workspace/mcp-tools.js create mode 100644 ai/workspace/public/component.html create mode 100644 ai/workspace/public/index.html create mode 100644 internal-packages/scripts/src/build-ai-workspace.js diff --git a/ai/workspace/.esbuild/index.js b/ai/workspace/.esbuild/index.js new file mode 100644 index 000000000..ce7f4bc32 --- /dev/null +++ b/ai/workspace/.esbuild/index.js @@ -0,0 +1,114 @@ +var index_default = ` + + + + + AI Workspace - Semantic UI Component Testing + + + + +
+

\u{1F916} AI Workspace

+

Simple testing environment for Semantic UI components.

+ +

Getting Started

+
    +
  1. Create component files in ai/workspace/components/my-component/
  2. +
  3. Use the standard pattern: component.js, component.html, component.css
  4. +
  5. Test your component at /component.html?name=my-component
  6. +
+ +

Quick Links

+ + +

Available Components

+
+

Loading components...

+
+
+ + \n\n"], + "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;", + "names": [] +} diff --git a/ai/workspace/README.md b/ai/workspace/README.md new file mode 100644 index 000000000..e08aabf7b --- /dev/null +++ b/ai/workspace/README.md @@ -0,0 +1,384 @@ +# AI Workspace - MCP Debugging Environment + +Complete web component debugging environment with Model Context Protocol (MCP) tools for AI agents. + +## ๐ŸŽฏ Features + +- **MCP WebSocket Server** - Real-time communication between AI agents and browser +- **Shadow DOM-Aware Debugging** - Pierces web component boundaries +- **Component Auto-Discovery** - Automatic registration and tracking +- **Real-Time Event Streaming** - Monitor component events as they happen +- **Code Execution in Context** - Run JavaScript in component scope +- **State Introspection** - Deep component state analysis +- **Mutation Tracking** - Track DOM changes over time +- **CSS Analysis** - Computed styles and custom properties inspection + +## ๐Ÿš€ Quick Start + +### Start the Development Environment + +```bash +npm run dev:workspace +``` + +This starts: +- **Web Server**: `http://localhost:8080` +- **MCP WebSocket**: `ws://localhost:8081` + +### Access the Interface + +- **Main Dashboard**: `http://localhost:8080/public/` +- **Component Tester**: `http://localhost:8080/public/component.html` +- **Test Component Demo**: `http://localhost:8080/public/component.html?name=test-component` + +## ๐Ÿ› ๏ธ MCP Tools for AI Agents + +### Core Inspection Tools + +#### `inspect_component` +Deep analysis of component structure, attributes, and shadow DOM. + +```javascript +{ + "tool": "inspect_component", + "params": { + "selector": "#my-component", + "depth": 3 + } +} +``` + +#### `execute_in_component` +Execute JavaScript code within component context. + +```javascript +{ + "tool": "execute_in_component", + "params": { + "selector": "test-component", + "code": "return { counter: host.counter, state: host.state }" + } +} +``` + +#### `monitor_events` +Real-time event monitoring with WebSocket streaming. + +```javascript +{ + "tool": "monitor_events", + "params": { + "selector": "#demo-test", + "events": ["click", "counter-changed", "section-toggled"] + } +} +``` + +### Shadow DOM Tools + +#### `query_shadow_dom` +Query elements within shadow DOM boundaries. + +```javascript +{ + "tool": "query_shadow_dom", + "params": { + "selector": "test-component", + "query": "button.increment-btn" + } +} +``` + +#### `get_computed_styles` +Analyze CSS styles and custom properties. + +```javascript +{ + "tool": "get_computed_styles", + "params": { + "selector": "test-component", + "elementPath": ".counter-display" + } +} +``` + +### State & History Tools + +#### `get_component_state` +Retrieve component's internal state and methods. + +```javascript +{ + "tool": "get_component_state", + "params": { + "selector": "#demo-test" + } +} +``` + +#### `mutation_history` +Track recent DOM changes within components. + +```javascript +{ + "tool": "mutation_history", + "params": { + "selector": "test-component", + "count": 10 + } +} +``` + +#### `list_components` +Discover all web components on the page. + +```javascript +{ + "tool": "list_components", + "params": {} +} +``` + +## ๐Ÿงฉ Creating Components + +### Standard Component Structure + +``` +ai/workspace/components/my-component/ +โ”œโ”€โ”€ component.js # Main component definition +โ”œโ”€โ”€ component.html # Template (optional) +โ””โ”€โ”€ component.css # Styles (optional) +``` + +### Example Component + +```javascript +// component.js +class MyComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._counter = 0; + } + + connectedCallback() { + this.render(); + } + + render() { + this.shadowRoot.innerHTML = ` + +
+

Counter: ${this._counter}

+ +
+ `; + } + + increment() { + this._counter++; + this.render(); + this.dispatchEvent(new CustomEvent('counter-changed', { + detail: { value: this._counter }, + bubbles: true, + composed: true + })); + } + + get counter() { return this._counter; } +} + +customElements.define('my-component', MyComponent); +``` + +## ๐Ÿ” Debugging Workflow + +### 1. Component Discovery +```javascript +// Find all components +await tools.list_components({}); + +// Inspect specific component +await tools.inspect_component({ + selector: "my-component", + depth: 2 +}); +``` + +### 2. Event Monitoring +```javascript +// Monitor user interactions +await tools.monitor_events({ + selector: "my-component", + events: ["click", "counter-changed"] +}); + +// Events stream to WebSocket automatically +``` + +### 3. State Analysis +```javascript +// Check component state +await tools.get_component_state({ + selector: "my-component" +}); + +// Execute custom code +await tools.execute_in_component({ + selector: "my-component", + code: "return { counter: host.counter, methods: Object.getOwnPropertyNames(host.__proto__) }" +}); +``` + +### 4. Style Debugging +```javascript +// Analyze computed styles +await tools.get_computed_styles({ + selector: "my-component", + elementPath: "button" +}); +``` + +### 5. Change Tracking +```javascript +// View recent mutations +await tools.mutation_history({ + selector: "my-component", + count: 5 +}); +``` + +## ๐Ÿ“ก WebSocket Communication + +### Message Types + +#### Tool Requests (Agent โ†’ Browser) +```javascript +{ + "type": "mcp-tool-request", + "tool": "inspect_component", + "params": { "selector": "my-component" }, + "id": "req-123" +} +``` + +#### Tool Responses (Browser โ†’ Agent) +```javascript +{ + "type": "mcp-tool-response", + "id": "req-123", + "result": { /* tool output */ } +} +``` + +#### Event Streams (Browser โ†’ Agent) +```javascript +{ + "type": "event-triggered", + "selector": "my-component", + "event": "counter-changed", + "detail": { + "type": "counter-changed", + "target": "MY-COMPONENT", + "detail": { "value": 5 }, + "timestamp": 1640995200000 + } +} +``` + +## ๐ŸŽฏ Test Components + +### Basic Test Component +- **Location**: `components/test-component/component.js` +- **Features**: Counter, themes, state management, events +- **Usage**: Perfect for learning MCP debugging + +### Example Component +- **Location**: `components/example/component.js` +- **Features**: Semantic UI integration, reactive templates +- **Usage**: Framework-specific patterns + +## ๐Ÿ”ง Technical Architecture + +### Debug Bridge (`debug-bridge.js`) +- Component auto-registration +- Shadow DOM traversal +- Event delegation and capture +- MCP tool request handling +- WebSocket communication + +### MCP Server (`build-ai-workspace.js`) +- WebSocket server on port 8081 +- Tool request routing +- Event broadcasting +- Connection management + +### esbuild Integration +- Debug bridge injection +- Live reloading +- Module serving +- Development server + +## ๐Ÿ“š Documentation References + +- **MCP Tools Schema**: `/mcp-tools.js` +- **Component Guide**: `/ai/guides/component-generation-instructions.md` +- **Query System**: `/ai/specialized/query-system-guide.md` +- **Mental Model**: `/ai/foundations/mental-model.md` + +## ๐ŸŽฎ Usage Examples + +### Debug a Broken Component +```javascript +// 1. Find the component +const components = await tools.list_components({}); + +// 2. Inspect its structure +const structure = await tools.inspect_component({ + selector: "broken-component" +}); + +// 3. Monitor for errors +await tools.monitor_events({ + selector: "broken-component", + events: ["error", "click", "change"] +}); + +// 4. Execute diagnostic code +const diagnostics = await tools.execute_in_component({ + selector: "broken-component", + code: ` + return { + hasErrors: !!shadow.querySelector('.error'), + eventListeners: getEventListeners ? getEventListeners(host) : 'Not available', + attributes: [...host.attributes].map(a => ({name: a.name, value: a.value})) + } + ` +}); +``` + +### Performance Analysis +```javascript +// Monitor mutations for performance issues +const mutations = await tools.mutation_history({ + selector: "heavy-component", + count: 20 +}); + +// Check computed styles for layout issues +const styles = await tools.get_computed_styles({ + selector: "heavy-component", + elementPath: ".performance-critical-element" +}); +``` + +--- + +## ๐ŸŽ‰ Ready to Debug! + +The MCP debugging environment provides comprehensive tools for AI agents to inspect, monitor, and debug web components with full shadow DOM support and real-time event streaming. + +Start the server with `npm run dev:workspace` and begin debugging! \ No newline at end of file diff --git a/ai/workspace/components/example/component.css b/ai/workspace/components/example/component.css new file mode 100644 index 000000000..4418ce34f --- /dev/null +++ b/ai/workspace/components/example/component.css @@ -0,0 +1,182 @@ +:host { + --component-bg: var(--background-color, #ffffff); + --component-border: var(--border-color, #e1e5e9); + --component-text: var(--text-color, #333333); + --primary-color: var(--primary, #007bff); + --secondary-color: var(--secondary, #6c757d); + --success-color: var(--success, #28a745); + --spacing: var(--spacing-md, 1rem); + --border-radius: var(--radius, 6px); + + display: block; + font-family: system-ui, -apple-system, sans-serif; +} + +.example-component { + background: var(--component-bg); + border: 1px solid var(--component-border); + border-radius: var(--border-radius); + padding: var(--spacing); + color: var(--component-text); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing); + padding-bottom: calc(var(--spacing) * 0.5); + border-bottom: 1px solid var(--component-border); +} + +.header h3 { + margin: 0; + color: var(--component-text); +} + +.content { + margin-bottom: var(--spacing); +} + +.counter { + text-align: center; + padding: var(--spacing); + background: #f8f9fa; + border-radius: var(--border-radius); + margin-bottom: var(--spacing); +} + +.counter p { + margin: 0 0 var(--spacing) 0; + font-size: 1.1rem; +} + +.increment-btn, .toggle-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +.increment-btn:hover, .toggle-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.increment-btn:active, .toggle-btn:active { + transform: translateY(0); +} + +.increment-btn:disabled, .toggle-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +/* Theme variations */ +.example-component.primary .increment-btn { + background: var(--primary-color); +} + +.example-component.secondary .increment-btn { + background: var(--secondary-color); +} + +.example-component.success .increment-btn { + background: var(--success-color); +} + +.expanded-content { + background: #f1f3f4; + padding: var(--spacing); + border-radius: var(--border-radius); + margin-top: var(--spacing); + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message-section { + margin-bottom: var(--spacing); +} + +.message-section label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.message-input { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--component-border); + border-radius: var(--border-radius); + font-size: 1rem; + box-sizing: border-box; +} + +.message-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0,123,255,0.25); +} + +.message-display { + margin-top: 0.5rem; + font-size: 0.9rem; + color: var(--secondary-color); +} + +.debug-info { + background: white; + padding: var(--spacing); + border-radius: var(--border-radius); + border: 1px solid var(--component-border); +} + +.debug-info h4 { + margin: 0 0 0.75rem 0; + color: var(--component-text); +} + +.debug-info ul { + margin: 0; + padding-left: 1.5rem; +} + +.debug-info li { + margin-bottom: 0.25rem; +} + +.debug-info code { + background: #e9ecef; + padding: 0.125rem 0.375rem; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.9rem; +} + +.footer { + text-align: center; + padding-top: calc(var(--spacing) * 0.5); + border-top: 1px solid var(--component-border); + color: var(--secondary-color); +} + +.footer small { + font-size: 0.8rem; +} \ No newline at end of file diff --git a/ai/workspace/components/example/component.html b/ai/workspace/components/example/component.html new file mode 100644 index 000000000..353806e65 --- /dev/null +++ b/ai/workspace/components/example/component.html @@ -0,0 +1,52 @@ +
+
+

{title}

+
+ +
+
+ +
+
+

Button clicked: {clicks} times

+ +
+ + {#if isExpanded} +
+
+ + +

Current: {message}

+
+ +
+

Debug Information:

+
    +
  • Theme: {theme}
  • +
  • Disabled: {disabled}
  • +
  • Expanded: {isExpanded}
  • +
  • Total clicks: {clicks}
  • +
+
+
+ {/if} +
+ + +
\ No newline at end of file diff --git a/ai/workspace/components/example/component.js b/ai/workspace/components/example/component.js new file mode 100644 index 000000000..185cfb12a --- /dev/null +++ b/ai/workspace/components/example/component.js @@ -0,0 +1,83 @@ +import { defineComponent, getText } from '/packages/component/src/index.js'; + +const template = await getText('./component.html'); +const css = await getText('./component.css'); + +const defaultSettings = { + title: 'Example Component', + theme: 'primary', + count: 0, + disabled: false, +}; + +const defaultState = { + isExpanded: false, + clicks: 0, + message: 'Hello from AI Workspace!', +}; + +const createComponent = ({ self, state, settings, dispatchEvent }) => ({ + increment() { + state.clicks.increment(); + dispatchEvent('countChanged', { + count: state.clicks.get(), + component: 'example', + }); + }, + + toggleExpanded() { + state.isExpanded.toggle(); + console.log('Expanded state:', state.isExpanded.get()); + }, + + updateMessage(newMessage) { + state.message.set(newMessage); + }, + + getStatus() { + return { + clicks: state.clicks.get(), + expanded: state.isExpanded.get(), + theme: settings.theme, + title: settings.title, + }; + }, +}); + +const events = { + 'click .increment-btn': ({ self }) => { + self.increment(); + }, + + 'click .toggle-btn': ({ self }) => { + self.toggleExpanded(); + }, + + 'input .message-input': ({ state, value }) => { + state.message.set(value); + }, +}; + +const onCreated = ({ state, settings }) => { + console.log('Example component created with settings:', settings); + state.clicks.set(settings.count || 0); +}; + +const onRendered = ({ self, isClient }) => { + if (isClient) { + console.log('Example component rendered in browser'); + console.log('Component status:', self.getStatus()); + } +}; + +export const ExampleComponent = defineComponent({ + tagName: 'example', + template, + css, + defaultSettings, + defaultState, + createComponent, + events, + onCreated, + onRendered, +}); diff --git a/ai/workspace/components/test-component/component.js b/ai/workspace/components/test-component/component.js new file mode 100644 index 000000000..24df95241 --- /dev/null +++ b/ai/workspace/components/test-component/component.js @@ -0,0 +1,599 @@ +/** + * Test Component for MCP Debugging Environment + * Demonstrates various web component patterns and debugging scenarios + */ + +class TestComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._counter = 0; + this._isExpanded = false; + this._theme = 'default'; + this._data = []; + this._timerId = null; + } + + static get observedAttributes() { + return ['theme', 'disabled', 'auto-increment']; + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + this.dispatchEvent( + new CustomEvent('test-component-connected', { + detail: { counter: this._counter }, + bubbles: true, + composed: true, + }), + ); + + // Auto-increment if enabled + if (this.hasAttribute('auto-increment')) { + this.startAutoIncrement(); + } + } + + disconnectedCallback() { + this.stopAutoIncrement(); + this.dispatchEvent( + new CustomEvent('test-component-disconnected', { + detail: { finalCounter: this._counter }, + bubbles: true, + composed: true, + }), + ); + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'theme': + this._theme = newValue || 'default'; + break; + case 'disabled': + this.disabled = newValue !== null; + break; + case 'auto-increment': + if (newValue !== null) { + this.startAutoIncrement(); + } + else { + this.stopAutoIncrement(); + } + break; + } + + if (this.shadowRoot) { + this.render(); + } + } + + render() { + this.shadowRoot.innerHTML = ` + + +
+
+ ๐Ÿงช Test Component + AUTO +
+
${this._theme}
+
+ +
+
+ + Counter: ${this._counter} +
+
+ + + + + +
+
+ +
+
+ ๐Ÿ“Š Data & State (Click to ${this._isExpanded ? 'collapse' : 'expand'}) + ${this._isExpanded ? 'โ–ฒ' : 'โ–ผ'} +
+
+
+ Internal State: +
+ Counter: ${this._counter} + Type: ${typeof this._counter} +
+
+ Expanded: ${this._isExpanded} + Theme: ${this._theme} +
+
+ Data Items: ${this._data.length} + Auto-increment: ${this.hasAttribute('auto-increment')} +
+
+ + + + + ${ + this._data.length > 0 + ? ` +
+ Data Array: + ${ + this._data.map((item, index) => ` +
+ ${index}: ${JSON.stringify(item)} + +
+ `).join('') + } +
+ ` + : '' + } +
+
+ +
+ ๐ŸŽฏ Slot content will appear here +
+ +
+Debug Info: +- Component ID: ${this.id || 'No ID set'} +- Classes: ${this.className || 'No classes'} +- Shadow DOM: ${this.shadowRoot ? 'Attached' : 'Not attached'} +- Connected: ${this.isConnected} +- Disabled: ${this.hasAttribute('disabled')} +- Render time: ${new Date().toLocaleTimeString()} +
+ `; + } + + setupEventListeners() { + const shadowRoot = this.shadowRoot; + + // Increment button + shadowRoot.getElementById('increment-btn')?.addEventListener('click', () => { + this.increment(); + }); + + // Increment by 5 + shadowRoot.getElementById('increment-5-btn')?.addEventListener('click', () => { + this.increment(5); + }); + + // Reset button + shadowRoot.getElementById('reset-btn')?.addEventListener('click', () => { + this.reset(); + }); + + // Random button + shadowRoot.getElementById('random-btn')?.addEventListener('click', () => { + this.setCounter(Math.floor(Math.random() * 100)); + }); + + // Error button (for testing error handling) + shadowRoot.getElementById('error-btn')?.addEventListener('click', () => { + this.triggerError(); + }); + + // Expand/collapse + shadowRoot.getElementById('expand-header')?.addEventListener('click', () => { + this.toggleExpanded(); + }); + + // Add data button + shadowRoot.getElementById('add-data-btn')?.addEventListener('click', () => { + this.addRandomData(); + }); + + // Clear data button + shadowRoot.getElementById('clear-data-btn')?.addEventListener('click', () => { + this.clearData(); + }); + + // Remove data buttons + shadowRoot.querySelectorAll('.remove-data').forEach(btn => { + btn.addEventListener('click', (e) => { + const index = parseInt(e.target.dataset.index); + this.removeData(index); + }); + }); + } + + // Public API methods + increment(amount = 1) { + const oldValue = this._counter; + this._counter += amount; + this.render(); + this.dispatchEvent( + new CustomEvent('counter-changed', { + detail: { + oldValue, + newValue: this._counter, + amount, + }, + bubbles: true, + composed: true, + }), + ); + } + + reset() { + const oldValue = this._counter; + this._counter = 0; + this.render(); + this.dispatchEvent( + new CustomEvent('counter-reset', { + detail: { oldValue }, + bubbles: true, + composed: true, + }), + ); + } + + setCounter(value) { + const oldValue = this._counter; + this._counter = value; + this.render(); + this.dispatchEvent( + new CustomEvent('counter-set', { + detail: { oldValue, newValue: value }, + bubbles: true, + composed: true, + }), + ); + } + + toggleExpanded() { + this._isExpanded = !this._isExpanded; + this.render(); + this.dispatchEvent( + new CustomEvent('section-toggled', { + detail: { expanded: this._isExpanded }, + bubbles: true, + composed: true, + }), + ); + } + + addRandomData() { + const newItem = { + id: Date.now(), + value: Math.floor(Math.random() * 1000), + timestamp: new Date().toISOString(), + type: ['string', 'number', 'boolean'][Math.floor(Math.random() * 3)], + }; + + this._data.push(newItem); + this.render(); + this.dispatchEvent( + new CustomEvent('data-added', { + detail: { item: newItem, total: this._data.length }, + bubbles: true, + composed: true, + }), + ); + } + + clearData() { + const count = this._data.length; + this._data = []; + this.render(); + this.dispatchEvent( + new CustomEvent('data-cleared', { + detail: { clearedCount: count }, + bubbles: true, + composed: true, + }), + ); + } + + removeData(index) { + if (index >= 0 && index < this._data.length) { + const removed = this._data.splice(index, 1)[0]; + this.render(); + this.dispatchEvent( + new CustomEvent('data-removed', { + detail: { item: removed, index, remaining: this._data.length }, + bubbles: true, + composed: true, + }), + ); + } + } + + startAutoIncrement() { + this.stopAutoIncrement(); + this._timerId = setInterval(() => { + this.increment(); + }, 2000); + this.dispatchEvent( + new CustomEvent('auto-increment-started', { + bubbles: true, + composed: true, + }), + ); + } + + stopAutoIncrement() { + if (this._timerId) { + clearInterval(this._timerId); + this._timerId = null; + this.dispatchEvent( + new CustomEvent('auto-increment-stopped', { + bubbles: true, + composed: true, + }), + ); + } + } + + triggerError() { + this.dispatchEvent( + new CustomEvent('error-triggered', { + detail: { + message: 'Intentional test error', + timestamp: Date.now(), + counter: this._counter, + }, + bubbles: true, + composed: true, + }), + ); + + // Simulate an actual error for testing + setTimeout(() => { + throw new Error('Test component intentional error for debugging'); + }, 100); + } + + // Getters for debugging + get counter() { + return this._counter; + } + + get theme() { + return this._theme; + } + + get isExpanded() { + return this._isExpanded; + } + + get data() { + return [...this._data]; // Return copy + } + + get state() { + return { + counter: this._counter, + theme: this._theme, + expanded: this._isExpanded, + dataCount: this._data.length, + autoIncrement: this.hasAttribute('auto-increment'), + disabled: this.hasAttribute('disabled'), + }; + } +} + +// Register the component +customElements.define('test-component', TestComponent); + +// Export for module usage +export { TestComponent }; diff --git a/ai/workspace/debug-bridge.js b/ai/workspace/debug-bridge.js new file mode 100644 index 000000000..fd9020d76 --- /dev/null +++ b/ai/workspace/debug-bridge.js @@ -0,0 +1,604 @@ +/** + * Debug Bridge - Browser-side debugging infrastructure for web components + * Provides MCP tool interface for component inspection and manipulation + */ +window.__debugBridge = { + ws: null, + componentRegistry: new WeakMap(), + eventListeners: new Map(), + mutationObservers: new Map(), + + connect(url) { + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + console.log('๐Ÿ”— Debug bridge connected to MCP server'); + }; + + this.ws.onmessage = async (event) => { + const { type, tool, params, id } = JSON.parse(event.data); + + if (type === 'mcp-tool-request') { + console.log(`๐Ÿ› ๏ธ Executing tool: ${tool}`, params); + try { + const result = await this.handleToolRequest(tool, params); + this.ws.send(JSON.stringify({ + type: 'mcp-tool-response', + id, + result, + })); + } + catch (error) { + this.ws.send(JSON.stringify({ + type: 'mcp-tool-response', + id, + error: { + message: error.message, + stack: error.stack, + params, + }, + })); + } + } + }; + + this.ws.onerror = (error) => { + console.error('โŒ Debug bridge WebSocket error:', error); + }; + + // Auto-register components + this.setupComponentTracking(); + }, + + setupComponentTracking() { + // Intercept customElements.define to track new component types + const originalDefine = customElements.define; + customElements.define = function(name, constructor, options) { + console.log(`๐Ÿ“ Registering component type: ${name}`); + originalDefine.call(this, name, constructor, options); + + // Track all existing instances + setTimeout(() => { + document.querySelectorAll(name).forEach(el => { + window.__debugBridge.registerComponent(el); + }); + }, 0); + }; + + // Monitor for new component instances + const observer = new MutationObserver((mutations) => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType === 1 && node.tagName.includes('-')) { + this.registerComponent(node); + } + // Also check for nested components + if (node.nodeType === 1 && node.querySelectorAll) { + node.querySelectorAll('*').forEach(child => { + if (child.tagName.includes('-')) { + this.registerComponent(child); + } + }); + } + }); + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + // Register existing components + document.querySelectorAll('*').forEach(el => { + if (el.tagName.includes('-')) { + this.registerComponent(el); + } + }); + }, + + registerComponent(element) { + if (this.componentRegistry.has(element)) { return; } + + console.log(`๐Ÿงฉ Registering component instance: ${element.tagName.toLowerCase()}`); + + const componentData = { + tagName: element.tagName.toLowerCase(), + shadowRoot: element.shadowRoot, + attributes: [...element.attributes].map(a => ({ + name: a.name, + value: a.value, + })), + properties: {}, + events: [], + mutations: [], + registeredAt: Date.now(), + }; + + this.componentRegistry.set(element, componentData); + + // Set up mutation observer for shadow DOM + if (element.shadowRoot) { + const observer = new MutationObserver((mutations) => { + const data = this.componentRegistry.get(element); + if (data) { + data.mutations.push(...mutations.map(m => ({ + type: m.type, + target: m.target.tagName || m.target.nodeName, + timestamp: Date.now(), + addedNodes: m.addedNodes.length, + removedNodes: m.removedNodes.length, + attributeName: m.attributeName, + oldValue: m.oldValue, + }))); + + // Keep only last 50 mutations + if (data.mutations.length > 50) { + data.mutations = data.mutations.slice(-50); + } + } + }); + + observer.observe(element.shadowRoot, { + childList: true, + attributes: true, + characterData: true, + subtree: true, + attributeOldValue: true, + characterDataOldValue: true, + }); + + this.mutationObservers.set(element, observer); + } + }, + + async handleToolRequest(tool, params) { + switch (tool) { + case 'inspect_component': + return this.inspectComponent(params.selector, params.depth); + + case 'execute_in_component': + return this.executeInComponent(params.selector, params.code); + + case 'monitor_events': + return this.monitorEvents(params.selector, params.events); + + case 'get_computed_styles': + return this.getComputedStyles(params.selector, params.elementPath); + + case 'mutation_history': + return this.getMutationHistory(params.selector, params.count); + + case 'list_components': + return this.listComponents(); + + case 'query_shadow_dom': + return this.queryShadowDOM(params.selector, params.query); + + case 'get_component_state': + return this.getComponentState(params.selector); + + case 'take_screenshot': + return this.takeScreenshot(params.selector); + + default: + throw new Error(`Unknown tool: ${tool}`); + } + }, + + // Enhanced $ function with shadow DOM piercing + $(selector, root = document) { + // If it's already an element, return it + if (selector instanceof Element) { return selector; } + + // Try normal query first + let element = root.querySelector(selector); + if (element) { return element; } + + // Deep search through shadow DOMs + const walker = document.createTreeWalker( + root, + NodeFilter.SHOW_ELEMENT, + { + acceptNode: (node) => { + if (node.shadowRoot) { + const found = node.shadowRoot.querySelector(selector); + if (found) { + element = found; + return NodeFilter.FILTER_REJECT; + } + } + return NodeFilter.FILTER_SKIP; + }, + }, + ); + + while (walker.nextNode() && !element) { + // Walker handles the traversal + } + + return element; + }, + + inspectComponent(selector, depth = 3) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + const data = this.componentRegistry.get(element); + if (!data) { + // Register it now if not already registered + this.registerComponent(element); + } + + const result = { + tagName: element.tagName.toLowerCase(), + id: element.id, + classes: [...element.classList], + attributes: [...element.attributes].map(a => ({ + name: a.name, + value: a.value, + })), + properties: this.getComponentProperties(element), + shadowRoot: null, + lightDOM: this.serializeDOM(element, Math.min(depth, 2)), + boundingRect: element.getBoundingClientRect(), + registeredEvents: data ? data.events.length : 0, + mutationCount: data ? data.mutations.length : 0, + }; + + if (element.shadowRoot && depth > 0) { + result.shadowRoot = this.serializeDOM(element.shadowRoot, depth - 1); + } + + return result; + }, + + executeInComponent(selector, code) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + // Create safe execution context + const context = { + host: element, + shadow: element.shadowRoot, + $: (sel) => this.$(sel, element.shadowRoot || element), + $$: (sel) => { + const results = []; + if (element.shadowRoot) { + results.push(...element.shadowRoot.querySelectorAll(sel)); + } + results.push(...element.querySelectorAll(sel)); + return results; + }, + document: element.shadowRoot || document, + getComputedStyle: window.getComputedStyle.bind(window), + console: { + log: (...args) => { + console.log('[Component Execution]', ...args); + return args; + }, + }, + }; + + // Execute with error handling + try { + const fn = new Function(...Object.keys(context), `"use strict"; return (${code})`); + return fn(...Object.values(context)); + } + catch (error) { + throw new Error(`Execution failed: ${error.message}`); + } + }, + + monitorEvents(selector, events) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + const key = `${selector}:${events.join(',')}`; + + // Remove existing listeners + if (this.eventListeners.has(key)) { + const oldListeners = this.eventListeners.get(key); + oldListeners.forEach(({ event, handler }) => { + element.removeEventListener(event, handler); + }); + } + + // Add new listeners + const listeners = events.map(event => { + const handler = (e) => { + const eventData = { + type: 'event-triggered', + selector, + event, + detail: { + type: e.type, + target: e.target.tagName || e.target.nodeName, + currentTarget: e.currentTarget.tagName || e.currentTarget.nodeName, + composed: e.composed, + bubbles: e.bubbles, + detail: e.detail, + timestamp: Date.now(), + path: e.composedPath ? e.composedPath().map(n => n.tagName || n.nodeName).slice(0, 5) : [], + }, + }; + + console.log('๐Ÿ“ก Event captured:', eventData); + + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(eventData)); + } + }; + + element.addEventListener(event, handler, true); + return { event, handler }; + }); + + this.eventListeners.set(key, listeners); + return { status: 'monitoring', events, selector, listenersActive: listeners.length }; + }, + + getComputedStyles(selector, elementPath) { + const component = this.$(selector); + if (!component) { throw new Error(`Component not found: ${selector}`); } + + let element = component; + + // Navigate to specific element within shadow DOM + if (elementPath) { + if (component.shadowRoot) { + element = component.shadowRoot.querySelector(elementPath); + } + else { + element = component.querySelector(elementPath); + } + if (!element) { throw new Error(`Element not found: ${elementPath}`); } + } + + const computed = window.getComputedStyle(element); + const styles = {}; + + // Get relevant computed styles (not all 300+ properties) + const importantProps = [ + 'display', + 'position', + 'width', + 'height', + 'margin', + 'padding', + 'border', + 'background', + 'color', + 'font-family', + 'font-size', + 'font-weight', + 'z-index', + 'opacity', + 'transform', + 'transition', + 'box-shadow', + 'border-radius', + 'overflow', + 'visibility', + ]; + + importantProps.forEach(prop => { + styles[prop] = computed.getPropertyValue(prop); + }); + + // Get CSS custom properties + const customProps = {}; + for (let i = 0; i < computed.length; i++) { + const prop = computed[i]; + if (prop.startsWith('--')) { + customProps[prop] = computed.getPropertyValue(prop); + } + } + + return { + selector, + elementPath, + tagName: element.tagName.toLowerCase(), + styles, + customProperties: customProps, + boundingBox: element.getBoundingClientRect(), + }; + }, + + getMutationHistory(selector, count = 10) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + const data = this.componentRegistry.get(element); + if (!data) { throw new Error(`Component not registered: ${selector}`); } + + return { + selector, + mutations: data.mutations.slice(-count), + total: data.mutations.length, + registeredAt: data.registeredAt, + }; + }, + + listComponents() { + const components = []; + + document.querySelectorAll('*').forEach(el => { + if (el.tagName.includes('-')) { + const data = this.componentRegistry.get(el); + components.push({ + tagName: el.tagName.toLowerCase(), + id: el.id || null, + classes: [...el.classList], + hasShadowRoot: !!el.shadowRoot, + isRegistered: !!data, + boundingRect: el.getBoundingClientRect(), + }); + } + }); + + return { + total: components.length, + components, + }; + }, + + queryShadowDOM(selector, query) { + const component = this.$(selector); + if (!component) { throw new Error(`Component not found: ${selector}`); } + + if (!component.shadowRoot) { + throw new Error(`Component has no shadow root: ${selector}`); + } + + const elements = [...component.shadowRoot.querySelectorAll(query)]; + + return { + selector, + query, + found: elements.length, + elements: elements.map(el => ({ + tagName: el.tagName.toLowerCase(), + id: el.id || null, + classes: [...el.classList], + attributes: [...el.attributes].map(a => ({ name: a.name, value: a.value })), + textContent: el.textContent.trim().substring(0, 100), + boundingRect: el.getBoundingClientRect(), + })), + }; + }, + + getComponentState(selector) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + // Try to get component state through common patterns + const state = { + properties: this.getComponentProperties(element), + attributes: [...element.attributes].map(a => ({ name: a.name, value: a.value })), + internalState: null, + methods: [], + }; + + // Look for common state patterns + if (element._state) { state.internalState = element._state; } + if (element.state) { state.internalState = element.state; } + if (element.data) { state.internalState = element.data; } + + // Get available methods + const proto = Object.getPrototypeOf(element); + Object.getOwnPropertyNames(proto).forEach(name => { + if ( + typeof element[name] === 'function' + && !name.startsWith('_') + && name !== 'constructor' + && !['addEventListener', 'removeEventListener', 'dispatchEvent'].includes(name) + ) { + state.methods.push(name); + } + }); + + return state; + }, + + takeScreenshot(selector) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + // Basic implementation - would need html2canvas for actual screenshots + const rect = element.getBoundingClientRect(); + + return { + selector, + boundingBox: { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + right: rect.right, + bottom: rect.bottom, + }, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + }, + note: 'For actual screenshots, html2canvas library would be needed', + }; + }, + + // Helper methods + serializeDOM(root, maxDepth, currentDepth = 0) { + if (currentDepth >= maxDepth) { return { type: 'truncated', maxDepthReached: true }; } + + const children = []; + for (const child of root.children || []) { + children.push({ + tagName: child.tagName.toLowerCase(), + id: child.id || null, + classes: [...child.classList], + attributes: [...child.attributes].map(a => ({ + name: a.name, + value: a.value, + })), + textContent: child.textContent ? child.textContent.trim().substring(0, 100) : '', + children: this.serializeDOM(child, maxDepth, currentDepth + 1), + }); + } + + return { + type: root.nodeType === 11 ? 'shadow-root' : 'element', + childCount: children.length, + children, + }; + }, + + getComponentProperties(element) { + const props = {}; + const proto = Object.getPrototypeOf(element); + + // Get all property descriptors + Object.getOwnPropertyNames(proto).forEach(name => { + if (name === 'constructor') { return; } + + try { + const descriptor = Object.getOwnPropertyDescriptor(proto, name); + if (descriptor && (descriptor.get || descriptor.value !== undefined)) { + const value = element[name]; + + // Only include serializable values + if ( + typeof value === 'string' + || typeof value === 'number' + || typeof value === 'boolean' + || value === null + || value === undefined + ) { + props[name] = value; + } + else if (typeof value === 'object' && value !== element) { + props[name] = '[Object]'; + } + else if (typeof value === 'function') { + props[name] = '[Function]'; + } + } + } + catch (e) { + // Some properties might throw + props[name] = '[Error accessing property]'; + } + }); + + return props; + }, +}; + +// Auto-connect when script loads +document.addEventListener('DOMContentLoaded', () => { + console.log('๐Ÿš€ Debug bridge initializing...'); + + // Make $ function globally available + window.$ = window.__debugBridge.$.bind(window.__debugBridge); + + console.log('โœ… Debug bridge ready'); +}); diff --git a/ai/workspace/mcp-tools.js b/ai/workspace/mcp-tools.js new file mode 100644 index 000000000..f9fd9a07b --- /dev/null +++ b/ai/workspace/mcp-tools.js @@ -0,0 +1,375 @@ +/** + * MCP Tool Definitions for Web Component Debugging + * These schemas define the available debugging tools for AI agents + */ + +export const debugTools = { + inspect_component: { + description: "Inspect a web component's structure, attributes, shadow DOM, and current state", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: + "CSS selector, component ID, or tag name to find the component. Examples: '#my-component', 'ui-dropdown', '.my-class'", + }, + depth: { + type: 'number', + description: 'How deep to traverse shadow DOM tree (default: 3, max recommended: 5)', + default: 3, + minimum: 1, + maximum: 10, + }, + }, + required: ['selector'], + }, + examples: [ + { + description: 'Inspect a component by ID', + parameters: { selector: '#main-dropdown', depth: 2 }, + }, + { + description: 'Deep inspection of component structure', + parameters: { selector: 'test-component', depth: 4 }, + }, + ], + }, + + execute_in_component: { + description: + "Execute JavaScript code within a component's context with access to shadow DOM and component internals", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector to execute code within', + }, + code: { + type: 'string', + description: + 'JavaScript code to execute. Available variables: host (component element), shadow (shadowRoot), $ (shadow-aware query), $$ (query all), document (shadow document), console', + }, + }, + required: ['selector', 'code'], + }, + examples: [ + { + description: "Check component's internal counter value", + parameters: { + selector: '#counter-component', + code: "host.counter || host._counter || 'Counter not found'", + }, + }, + { + description: 'Find all buttons in shadow DOM', + parameters: { + selector: 'ui-modal', + code: "$$('button').length", + }, + }, + { + description: "Get component's current state", + parameters: { + selector: 'test-component', + code: '({ counter: host.counter, shadowHTML: shadow.innerHTML.length, classList: [...host.classList] })', + }, + }, + ], + }, + + monitor_events: { + description: 'Start monitoring specific events on a component. Events will be streamed in real-time via WebSocket', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector to monitor', + }, + events: { + type: 'array', + items: { type: 'string' }, + description: 'Array of event names to monitor. Common events: click, change, input, custom events', + }, + }, + required: ['selector', 'events'], + }, + examples: [ + { + description: 'Monitor user interactions', + parameters: { + selector: '#interactive-component', + events: ['click', 'mouseover', 'focus'], + }, + }, + { + description: 'Monitor custom component events', + parameters: { + selector: 'ui-dropdown', + events: ['selection-changed', 'dropdown-opened', 'dropdown-closed'], + }, + }, + ], + }, + + get_computed_styles: { + description: 'Get computed CSS styles for a component or element within its shadow DOM', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + elementPath: { + type: 'string', + description: + "Optional CSS selector for element within component's shadow DOM. If not provided, styles of the component itself are returned", + }, + }, + required: ['selector'], + }, + examples: [ + { + description: "Get component's own styles", + parameters: { selector: 'test-component' }, + }, + { + description: 'Get styles of button inside component', + parameters: { + selector: 'test-component', + elementPath: 'button', + }, + }, + { + description: 'Get styles of specific element', + parameters: { + selector: 'ui-modal', + elementPath: '.modal-header .close-button', + }, + }, + ], + }, + + mutation_history: { + description: "Get recent DOM mutations within a component's shadow DOM for debugging dynamic changes", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + count: { + type: 'number', + description: 'Number of recent mutations to retrieve (default: 10, max: 50)', + default: 10, + minimum: 1, + maximum: 50, + }, + }, + required: ['selector'], + }, + examples: [ + { + description: 'Get last 5 DOM changes', + parameters: { selector: 'dynamic-component', count: 5 }, + }, + { + description: 'Get full mutation history', + parameters: { selector: 'test-component', count: 50 }, + }, + ], + }, + + list_components: { + description: 'List all web components currently on the page with their basic information', + parameters: { + type: 'object', + properties: {}, + additionalProperties: false, + }, + examples: [ + { + description: 'Get overview of all components', + parameters: {}, + }, + ], + }, + + query_shadow_dom: { + description: "Query for elements within a component's shadow DOM using CSS selectors", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + query: { + type: 'string', + description: "CSS selector to find elements within the component's shadow DOM", + }, + }, + required: ['selector', 'query'], + }, + examples: [ + { + description: 'Find all buttons in component', + parameters: { + selector: 'ui-modal', + query: 'button', + }, + }, + { + description: 'Find elements with specific class', + parameters: { + selector: 'test-component', + query: '.interactive-element', + }, + }, + { + description: 'Find form inputs', + parameters: { + selector: 'ui-form', + query: 'input, select, textarea', + }, + }, + ], + }, + + get_component_state: { + description: "Get a component's internal state, properties, and available methods for debugging", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + }, + required: ['selector'], + }, + examples: [ + { + description: "Get component's current state", + parameters: { selector: 'stateful-component' }, + }, + { + description: 'Check component properties and methods', + parameters: { selector: '#dynamic-widget' }, + }, + ], + }, + + take_screenshot: { + description: + 'Get component dimensions and position information (note: actual screenshot requires additional libraries)', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + }, + required: ['selector'], + }, + examples: [ + { + description: 'Get component layout info', + parameters: { selector: 'ui-modal' }, + }, + ], + }, +}; + +/** + * Tool usage guidelines for AI agents + */ +export const usageGuidelines = { + debugging_workflow: [ + "1. Start with 'list_components' to see all available components", + "2. Use 'inspect_component' to understand structure and current state", + "3. Use 'execute_in_component' to test specific functionality", + "4. Monitor behavior with 'monitor_events' for interactive debugging", + "5. Check styling issues with 'get_computed_styles'", + "6. Track dynamic changes with 'mutation_history'", + ], + + best_practices: { + selectors: [ + 'Use specific selectors when possible (IDs or tag names)', + "For custom components, use tag names like 'ui-dropdown' or 'test-component'", + "Fallback to class selectors '.my-component' if needed", + ], + + code_execution: [ + 'Keep executed code simple and focused', + 'Use the provided context variables (host, shadow, $, $$)', + 'Return serializable data (avoid circular references)', + 'Use console.log for debugging output', + ], + + event_monitoring: [ + 'Start with common events: click, change, input', + 'Add custom component events based on component documentation', + 'Monitor for short periods to avoid overwhelming the system', + ], + }, + + common_patterns: { + finding_components: 'list_components() โ†’ inspect_component(selector)', + debugging_interactions: "monitor_events(selector, ['click']) โ†’ execute_in_component(selector, code)", + styling_issues: 'inspect_component(selector) โ†’ get_computed_styles(selector, elementPath)', + state_debugging: "get_component_state(selector) โ†’ execute_in_component(selector, 'return host.someProperty')", + }, +}; + +/** + * Event types that components commonly emit + */ +export const commonEvents = { + user_interactions: [ + 'click', + 'dblclick', + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'focus', + 'blur', + 'keydown', + 'keyup', + 'input', + 'change', + 'submit', + ], + + semantic_ui_components: [ + 'selection-changed', + 'item-selected', + 'dropdown-opened', + 'dropdown-closed', + 'modal-opened', + 'modal-closed', + 'tab-changed', + 'accordion-expanded', + 'form-validated', + 'field-changed', + 'button-clicked', + ], + + lifecycle_events: [ + 'component-created', + 'component-rendered', + 'component-destroyed', + 'state-changed', + 'settings-updated', + 'data-loaded', + ], +}; + +export default debugTools; diff --git a/ai/workspace/public/component.html b/ai/workspace/public/component.html new file mode 100644 index 000000000..2b5ab7938 --- /dev/null +++ b/ai/workspace/public/component.html @@ -0,0 +1,447 @@ + + + + + + Component Tester - AI Workspace + + + + +
+
+
+

Component Tester

+ No component loaded +
+
+ + +
+
+ +
+ + +
+

Loading component...

+
+
+
+ +
+
+

๐Ÿ› ๏ธ Debug Tools

+
+ +
+
+

Query Tester

+ +

+ + +
Run a query to see results...
+
+ +
+

Component Inspector

+ + +
Click a button to inspect components...
+
+ +
+

Shadow DOM Explorer

+ + +
Explore shadow DOM structure...
+
+ +
+

Console Log

+ +
Console output will appear here...
+
+
+
+ +
+

Build Error

+
+

+
+ + + + \ No newline at end of file diff --git a/ai/workspace/public/index.html b/ai/workspace/public/index.html new file mode 100644 index 000000000..4521baf0a --- /dev/null +++ b/ai/workspace/public/index.html @@ -0,0 +1,384 @@ + + + + + + AI Workspace - MCP Debugging Environment + + + +
+ ๐Ÿ”Œ Connecting to MCP... +
+ +
+
+

๐Ÿงช AI Workspace

+

MCP-Enabled Web Component Debugging Environment

+
+ +
+
+
+
WebSocket
+ Connecting... +
+
+
+
Debug Bridge
+ Initializing... +
+
+
+
Components
+ Scanning... +
+
+ +
+

๐Ÿ› ๏ธ Available MCP Tools

+

AI agents can use these tools to inspect and debug web components:

+
+
+ inspect_component
+ Deep component analysis +
+
+ execute_in_component
+ Run code in component context +
+
+ monitor_events
+ Real-time event streaming +
+
+ query_shadow_dom
+ Shadow DOM querying +
+
+ get_computed_styles
+ CSS inspection +
+
+ list_components
+ Component discovery +
+
+
+ +
+

๐ŸŽฏ Live Test Component

+

Interact with this component while an AI agent debugs it in real-time:

+ +

๐ŸŽช This is slotted content inside the test component!

+
+
+ +
+
+

๐Ÿš€ Quick Start

+
    +
  1. Create components in components/my-component/
  2. +
  3. Use MCP tools to inspect and debug
  4. +
  5. Test with real-time event monitoring
  6. +
  7. Iterate based on debugging insights
  8. +
+
+ + + +
+

๐Ÿ“Š Debug Features

+
    +
  • ๐Ÿ” Shadow DOM inspection
  • +
  • โšก Real-time event monitoring
  • +
  • ๐ŸŽจ CSS computed styles analysis
  • +
  • ๐Ÿง  Component state introspection
  • +
  • ๐Ÿ’พ DOM mutation tracking
  • +
  • ๐ŸŽฏ Code execution in component context
  • +
+
+ +
+

๐Ÿ“š Documentation

+

Access comprehensive component development guides:

+ +
+
+ +
+

๐Ÿ” Discovered Components

+
+

Scanning for components...

+
+
+
+ + + + + \ No newline at end of file diff --git a/internal-packages/scripts/src/build-ai-workspace.js b/internal-packages/scripts/src/build-ai-workspace.js new file mode 100644 index 000000000..479873001 --- /dev/null +++ b/internal-packages/scripts/src/build-ai-workspace.js @@ -0,0 +1,184 @@ +import * as esbuild from 'esbuild'; +import fs from 'fs/promises'; +import { resolve } from 'path'; +import { WebSocketServer } from 'ws'; +import { build } from './lib/build.js'; + +const BASE_DIR = process.env.BASE_DIR || process.cwd(); +const WORKSPACE_DIR = resolve(BASE_DIR, 'ai/workspace'); + +/* + MCP-enabled debugging environment for AI workspace + Sets up WebSocket server for MCP tools and esbuild serve with debug injection +*/ +export const buildAIWorkspace = async ({ + serve = false, + port = 8080, + wsPort = 8081, + host = 'localhost', + buildDeps = false, + watch = false, +} = {}) => { + if (serve) { + console.log('๐Ÿš€ Starting AI Workspace with MCP debugging...'); + + // Set up WebSocket server for MCP tool communication + const wss = new WebSocketServer({ port: wsPort }); + const connections = new Set(); + + console.log(`๐Ÿ”Œ MCP WebSocket server running on ws://${host}:${wsPort}`); + + wss.on('connection', (ws) => { + connections.add(ws); + console.log('๐Ÿ”— Debug bridge connected'); + + ws.on('close', () => { + connections.delete(ws); + console.log('๐Ÿ”Œ Debug bridge disconnected'); + }); + + // Handle MCP tool requests from agents + ws.on('message', async (data) => { + try { + const message = JSON.parse(data); + + if (message.type === 'mcp-tool-request') { + console.log(`๐Ÿ› ๏ธ Agent tool request: ${message.tool}`); + + // Forward to browser debug bridge + for (const conn of connections) { + if (conn !== ws && conn.readyState === 1) { + conn.send(data); + } + } + } + else if (message.type === 'mcp-tool-response') { + // Forward response back to requesting agent + for (const conn of connections) { + if (conn !== ws && conn.readyState === 1) { + conn.send(data); + } + } + } + else if (message.type === 'event-triggered') { + // Broadcast events to all connected agents + console.log(`๐Ÿ“ก Component event: ${message.event} on ${message.selector}`); + for (const conn of connections) { + if (conn !== ws && conn.readyState === 1) { + conn.send(data); + } + } + } + } + catch (error) { + console.error('โŒ WebSocket message error:', error); + } + }); + }); + + // esbuild plugin to inject debug bridge + const debugBridgePlugin = { + name: 'debug-bridge-injector', + setup(build) { + build.onLoad({ filter: /\.html$/ }, async (args) => { + let content = await fs.readFile(args.path, 'utf8'); + + // Inject debug bridge script before + const injection = ` + + + `; + + content = content.replace('', injection + ''); + return { contents: content, loader: 'html' }; + }); + }, + }; + + try { + const context = await esbuild.context({ + entryPoints: [ + resolve(WORKSPACE_DIR, 'public/index.html'), + resolve(WORKSPACE_DIR, 'debug-bridge.js'), + resolve(WORKSPACE_DIR, 'mcp-tools.js'), + ], + bundle: false, + format: 'esm', + outdir: resolve(WORKSPACE_DIR, '.esbuild'), + plugins: [debugBridgePlugin], + logLevel: 'info', + loader: { + '.html': 'text', + '.css': 'text', + '.js': 'js', + '.ts': 'ts', + }, + }); + + if (watch) { + await context.watch(); + console.log('๐Ÿ‘€ File watching enabled'); + } + + const result = await context.serve({ + port, + host, + servedir: WORKSPACE_DIR, + }); + + console.log(`\nโœ… AI Workspace Ready!`); + console.log(`๐Ÿ“ก Web server: http://${result.host}:${result.port}`); + console.log(`๐Ÿ”Œ MCP WebSocket: ws://${host}:${wsPort}`); + console.log(`๐Ÿ“ Serving from: ${WORKSPACE_DIR}`); + console.log(`\n๐ŸŽฏ Quick Links:`); + console.log(` โ€ข Main page: http://localhost:${port}/public/`); + console.log(` โ€ข Component tester: http://localhost:${port}/public/component.html`); + console.log(` โ€ข Test component: http://localhost:${port}/public/component.html?name=test-component`); + console.log(`\n๐Ÿ’ก MCP Tools Available:`); + console.log(` โ€ข inspect_component - Deep component analysis`); + console.log(` โ€ข execute_in_component - Run code in component context`); + console.log(` โ€ข monitor_events - Real-time event streaming`); + console.log(` โ€ข list_components - See all components on page`); + console.log(` โ€ข See mcp-tools.js for complete tool reference`); + + // Keep the process alive + return new Promise(() => { + // Graceful shutdown handling + process.on('SIGINT', () => { + console.log('\n๐Ÿ›‘ Shutting down AI workspace...'); + wss.close(); + context.dispose(); + process.exit(0); + }); + }); + } + catch (error) { + console.error('โŒ Failed to start workspace:', error); + wss.close(); + return { success: false, error }; + } + } + + return { success: true }; +}; + +// Handle direct execution of this script +if (import.meta.url === `file://${process.argv[1]}`) { + (async function() { + const serve = process.argv.includes('--serve'); + const port = process.argv.includes('--port') + ? parseInt(process.argv[process.argv.indexOf('--port') + 1]) + : 8080; + + return buildAIWorkspace({ + serve, + port, + watch: serve, // Enable watch mode when serving + }); + })(); +} diff --git a/internal-packages/scripts/src/index.js b/internal-packages/scripts/src/index.js index ed4865585..9528463a8 100644 --- a/internal-packages/scripts/src/index.js +++ b/internal-packages/scripts/src/index.js @@ -2,11 +2,12 @@ export * from './lib/index.js'; /* Builders */ +export { buildAIWorkspace } from './build-ai-workspace.js'; export { buildBundle } from './build-bundle.js'; export { buildCDN } from './build-cdn.js'; export { buildESM } from './build-esm.js'; -export { buildUIDeps } from './build-ui-deps.js'; export { buildUIComponents } from './build-ui-components.js'; +export { buildUIDeps } from './build-ui-deps.js'; /* Watch */ export { watch } from './watch.js'; diff --git a/package-lock.json b/package-lock.json index 7dfc92ccb..a58a93ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,8 @@ "simple-git-hooks": "^2.12.1", "underscore": "^1.13.7", "vitest": "^3.0.9", - "wireit": "^0.14.11" + "wireit": "^0.14.11", + "ws": "^8.18.2" } }, "internal-packages/esbuild-callback": { @@ -8029,7 +8030,9 @@ } }, "node_modules/ws": { - "version": "8.18.1", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index ec8a68074..444509476 100755 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "scripts": { "build": "wireit", "dev": "wireit", + "dev:workspace": "node ./internal-packages/scripts/src/build-ai-workspace.js --serve", "build:docs": "wireit", "build:packages": "wireit", "watch": "wireit", @@ -218,7 +219,8 @@ "simple-git-hooks": "^2.12.1", "underscore": "^1.13.7", "vitest": "^3.0.9", - "wireit": "^0.14.11" + "wireit": "^0.14.11", + "ws": "^8.18.2" }, "ciDependencies": { "axios": "^1.6.8", From 4f45731a697963a8311efd55b0c832ba524b4f62 Mon Sep 17 00:00:00 2001 From: jlukic Date: Sun, 22 Jun 2025 21:56:03 -0400 Subject: [PATCH 03/12] Docs: Proposal for changes to query --- ai/proposals/query-core-missing-methods.md | 105 ++++++++++ ai/proposals/query-css-animations.md | 135 +++++++++++++ ai/proposals/query-missing-methods.md | 211 +++++++++++++++++++++ ai/proposals/query-positioning.md | 159 ++++++++++++++++ ai/specialized/query-system-guide.md | 142 ++++++++------ 5 files changed, 694 insertions(+), 58 deletions(-) create mode 100644 ai/proposals/query-core-missing-methods.md create mode 100644 ai/proposals/query-css-animations.md create mode 100644 ai/proposals/query-missing-methods.md create mode 100644 ai/proposals/query-positioning.md diff --git a/ai/proposals/query-core-missing-methods.md b/ai/proposals/query-core-missing-methods.md new file mode 100644 index 000000000..81990c015 --- /dev/null +++ b/ai/proposals/query-core-missing-methods.md @@ -0,0 +1,105 @@ +# Query Core - Missing Methods Proposal + +> **Scope:** Essential missing methods with minimal complexity +> **Status:** Draft for implementation +> **Size Impact:** Minimal - mostly aliases and simple logic reuse + +--- + +## Array-like Operations + +### `slice(start, end)` +Get subset of elements as new Query instance. + +- `start` - Number starting index +- `end` - Number ending index (optional) + +**Implementation:** Simple Array.slice() wrapper + +### `splice(start, deleteCount, ...items)` +Modify the element collection in place. + +- `start` - Number starting index +- `deleteCount` - Number of elements to remove +- `items` - Elements to insert at start position + +**Implementation:** Array.splice() with Query wrapping + +--- + +## Enhanced Traversal + +### `parents(selector)` +Get all ancestor elements, optionally filtered. Respects `this.options.pierceShadow`. + +- `selector` - String CSS selector to filter ancestors (optional) + +**Implementation:** Follow `parent()` pattern but collect all ancestors, not just immediate parent + +--- + +## DOM Insertion Aliases + +### `before(content)` +Alias for `insertBefore()` with more intuitive name. + +- `content` - String HTML | Element | Query | Array of content + +**Implementation:** Simple alias to existing insertBefore logic + +### `after(content)` +Alias for `insertAfter()` with more intuitive name. + +- `content` - String HTML | Element | Query | Array of content + +**Implementation:** Simple alias to existing insertAfter logic + +--- + +## Enhanced Dimensions + +### `width(options)` / `height(options)` +Extend existing methods with inclusion options. + +- `options.includeMargin` - Boolean to include margin +- `options.includePadding` - Boolean to include padding +- `options.includeBorder` - Boolean to include border + +**Implementation:** Extend existing width/height with computed style calculations + +--- + +## Data Attribute Helpers + +### `data(key, value)` +Get/set data-* attributes with type conversion. + +- `key` - String attribute name or Object of key-value pairs +- `value` - Any value to set, undefined to get + +**Implementation:** Wrapper around attr() with 'data-' prefix and JSON parsing + +### `data()` +Get all data-* attributes as object. + +**Implementation:** Filter attributes starting with 'data-', parse values + +--- + +## Shadow DOM Enhancements + +### `contains(selector)` +Check if elements contain targets. Automatically shadow DOM aware based on `this.options.pierceShadow`. + +- `selector` - String | Element | Query to check containment + +**Implementation:** Use DOM `.contains()` or deep traversal based on `this.options.pierceShadow` + +--- + +## Implementation Notes + +- **File size impact:** < 2KB gzipped +- **Complexity:** Low - mostly aliases and logical extensions +- **Dependencies:** None - uses existing Query infrastructure +- **Breaking changes:** None - all additions \ No newline at end of file diff --git a/ai/proposals/query-css-animations.md b/ai/proposals/query-css-animations.md new file mode 100644 index 000000000..033f8ddb0 --- /dev/null +++ b/ai/proposals/query-css-animations.md @@ -0,0 +1,135 @@ +# Query CSS Animations Extension + +> **Scope:** CSS-based animations and transitions integration +> **Status:** Complex scope requiring design decisions +> **Size Impact:** Moderate - style injection and state management logic + +--- + +## Core Animation Methods + +### `show(options)` +Show elements with CSS animation support. + +- `options.animation` - String CSS animation class or transition property +- `options.duration` - String CSS duration ('300ms', '0.3s') +- `options.timing` - String CSS timing function ('ease', 'cubic-bezier(...)') +- `options.display` - String target display value ('block', 'flex', 'inline-block') + +### `hide(options)` +Hide elements with CSS animation support. + +- `options.animation` - String CSS animation class or transition property +- `options.duration` - String CSS duration +- `options.timing` - String CSS timing function + +### `toggle(force, options)` +Toggle visibility with animation support. + +- `force` - Boolean to force show/hide, undefined for toggle +- `options` - Same as show/hide options + +### `visible()` +Check if elements are visible. + +**Returns:** Boolean true if any element is visible + +--- + +## Design Challenges + +### Display State Resolution +**Problem:** When showing `display: none` elements, what should the final display value be? + +**Options:** +1. **Store original display** - Remember pre-hide display value +2. **Compute from CSS** - Analyze stylesheets for intended display +3. **Default + override** - Default to 'block', allow explicit override +4. **Element-based heuristics** - div=block, span=inline, etc. + +### Style Injection Strategy +**Problem:** How to inject temporary animation styles? + +**Options:** +1. **Inline styles** - Direct element.style modification +2. **Style sheet injection** - Dynamic `