diff --git a/packages/domscribe-relay/src/mcp/prompts/find-annotations.prompt.ts b/packages/domscribe-relay/src/mcp/prompts/find-annotations.prompt.ts index 0cc4d60..329c614 100644 --- a/packages/domscribe-relay/src/mcp/prompts/find-annotations.prompt.ts +++ b/packages/domscribe-relay/src/mcp/prompts/find-annotations.prompt.ts @@ -30,7 +30,7 @@ export class FindAnnotationsPrompt implements McpPromptDefinition< type: 'text', text: `Search for Domscribe annotations. -Use the domscribe.annotations.search tool with these filters: +Use the domscribe.annotation.search tool with these filters: ${args.query ? `- query: "${args.query}"` : ''} ${args.file ? `- file: "${args.file}"` : ''} ${args.entryId ? `- entryId: "${args.entryId}"` : ''} diff --git a/packages/domscribe-relay/src/mcp/prompts/process-next.prompt.ts b/packages/domscribe-relay/src/mcp/prompts/process-next.prompt.ts index f70f52c..41c83cc 100644 --- a/packages/domscribe-relay/src/mcp/prompts/process-next.prompt.ts +++ b/packages/domscribe-relay/src/mcp/prompts/process-next.prompt.ts @@ -6,9 +6,9 @@ import { const ProcessNextArgsSchema = {}; -export class ProcessNextPrompt - implements McpPromptDefinition -{ +export class ProcessNextPrompt implements McpPromptDefinition< + typeof ProcessNextArgsSchema +> { name = MCP_PROMPTS.PROCESS_NEXT; description = 'Process the next queued UI annotation. Claims and processes one annotation from the queue.'; @@ -23,14 +23,14 @@ export class ProcessNextPrompt type: 'text', text: `Process the next queued Domscribe annotation. -Use the domscribe.annotations.process tool to claim the next annotation. +Use the domscribe.annotation.process tool to claim the next annotation. If an annotation is found: 1. Read the userIntent and sourceLocation 2. Navigate to the source file and understand the context 3. Implement the requested change -4. Use domscribe.annotations.respond to store your response -5. Use domscribe.annotations.updateStatus to mark it as 'processed' +4. Use domscribe.annotation.respond to store your response +5. Use domscribe.annotation.updateStatus to mark it as 'processed' If no annotation is found, inform the user that the queue is empty.`, }, diff --git a/packages/domscribe-relay/src/mcp/prompts/prompts.spec.ts b/packages/domscribe-relay/src/mcp/prompts/prompts.spec.ts index f41d218..fc5acab 100644 --- a/packages/domscribe-relay/src/mcp/prompts/prompts.spec.ts +++ b/packages/domscribe-relay/src/mcp/prompts/prompts.spec.ts @@ -20,10 +20,10 @@ describe('ProcessNextPrompt', () => { expect(messages).toHaveLength(1); expect(messages[0].role).toBe('user'); expect(messages[0].content.type).toBe('text'); - expect(messages[0].content.text).toContain('domscribe.annotations.process'); - expect(messages[0].content.text).toContain('domscribe.annotations.respond'); + expect(messages[0].content.text).toContain('domscribe.annotation.process'); + expect(messages[0].content.text).toContain('domscribe.annotation.respond'); expect(messages[0].content.text).toContain( - 'domscribe.annotations.updateStatus', + 'domscribe.annotation.updateStatus', ); }); }); diff --git a/packages/domscribe-relay/src/mcp/tools/annotation-list.tool.ts b/packages/domscribe-relay/src/mcp/tools/annotation-list.tool.ts index 13aad61..b544b8c 100644 --- a/packages/domscribe-relay/src/mcp/tools/annotation-list.tool.ts +++ b/packages/domscribe-relay/src/mcp/tools/annotation-list.tool.ts @@ -37,18 +37,15 @@ type AnnotationsListToolOutput = z.infer< typeof AnnotationsListToolOutputSchema >; -export class AnnotationsListTool - implements - McpToolDefinition< - typeof AnnotationsListToolInputSchema, - typeof AnnotationsListToolOutputSchema - > -{ +export class AnnotationsListTool implements McpToolDefinition< + typeof AnnotationsListToolInputSchema, + typeof AnnotationsListToolOutputSchema +> { name = MCP_TOOLS.ANNOTATION_LIST; description = - 'List Domscribe annotations with optional status filtering and pagination. ' + - 'Annotations are user-captured UI interactions awaiting or completed by agent processing. ' + - 'Use to see pending work (status: queued), in-progress items, or review completed/failed tasks.'; + 'List Domscribe annotations for monitoring and review purposes. ' + + 'To process the NEXT queued annotation, use domscribe.annotation.process instead — it atomically claims and returns full context in one call. ' + + 'Use this tool only to browse queue state, check counts, or review history by status.'; inputSchema = AnnotationsListToolInputSchema; outputSchema = AnnotationsListToolOutputSchema; diff --git a/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.spec.ts b/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.spec.ts index 05c2e26..d75eb0d 100644 --- a/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.spec.ts +++ b/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.spec.ts @@ -41,7 +41,13 @@ describe('AnnotationsProcessTool', () => { // Assert expect(mockClient.processAnnotation).toHaveBeenCalled(); - expect(result.structuredContent).toEqual(processResponse); + expect(result.structuredContent).toEqual({ + ...processResponse, + nextStep: + 'Implement the change described in userIntent. ' + + 'Then call domscribe.query.bySource with the same file and line to verify your changes in the live browser. ' + + 'Then call domscribe.annotation.respond with your summary, then domscribe.annotation.updateStatus with status "processed".', + }); }); it('should handle empty queue', async () => { diff --git a/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.ts b/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.ts index 9275c3e..c5d83ad 100644 --- a/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.ts +++ b/packages/domscribe-relay/src/mcp/tools/annotation-process.tool.ts @@ -63,24 +63,26 @@ const AnnotationsProcessToolOutputSchema = McpToolOutputSchema.extend({ .unknown() .optional() .describe('Full annotation for advanced use cases'), + nextStep: z + .string() + .optional() + .describe('Workflow hint — what to do after this tool call'), }); type AnnotationsProcessToolOutput = z.infer< typeof AnnotationsProcessToolOutputSchema >; -export class AnnotationsProcessTool - implements - McpToolDefinition< - typeof AnnotationsProcessToolInputSchema, - typeof AnnotationsProcessToolOutputSchema - > -{ +export class AnnotationsProcessTool implements McpToolDefinition< + typeof AnnotationsProcessToolInputSchema, + typeof AnnotationsProcessToolOutputSchema +> { name = MCP_TOOLS.ANNOTATION_PROCESS; description = - 'Atomically claim the next queued annotation for processing. ' + + 'Claim the next queued annotation for processing (atomic — no annotation ID needed). ' + + 'This is the correct tool for picking up work. Do NOT use annotation.list to manually pick annotations. ' + 'Returns the oldest queued annotation with full context including resolved source location. ' + - 'Use when ready to process the next user request from the queue.'; + 'After implementing the change, call annotation.respond then annotation.updateStatus to complete the lifecycle.'; inputSchema = AnnotationsProcessToolInputSchema; outputSchema = AnnotationsProcessToolOutputSchema; @@ -100,6 +102,11 @@ export class AnnotationsProcessTool sourceLocation: response.sourceLocation, runtimeContext: response.runtimeContext, fullAnnotation: response.fullAnnotation, + nextStep: response.found + ? 'Implement the change described in userIntent. ' + + 'Then call domscribe.query.bySource with the same file and line to verify your changes in the live browser. ' + + 'Then call domscribe.annotation.respond with your summary, then domscribe.annotation.updateStatus with status "processed".' + : undefined, }; return { diff --git a/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.spec.ts b/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.spec.ts index 772dca3..98f373d 100644 --- a/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.spec.ts +++ b/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.spec.ts @@ -31,6 +31,8 @@ describe('AnnotationsRespondTool', () => { expect(result.structuredContent).toEqual({ success: true, annotationId: 'ann_123', + nextStep: + 'Call domscribe.annotation.updateStatus with annotationId "ann_123" and status "processed" to complete the lifecycle.', }); }); diff --git a/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.ts b/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.ts index 62ece35..ecbe273 100644 --- a/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.ts +++ b/packages/domscribe-relay/src/mcp/tools/annotation-respond.tool.ts @@ -25,23 +25,25 @@ const AnnotationsRespondToolOutputSchema = McpToolOutputSchema.extend({ .string() .optional() .describe('The annotation ID that received the response'), + nextStep: z + .string() + .optional() + .describe('Workflow hint — what to do after this tool call'), }); type AnnotationsRespondToolOutput = z.infer< typeof AnnotationsRespondToolOutputSchema >; -export class AnnotationsRespondTool - implements - McpToolDefinition< - typeof AnnotationsRespondToolInputSchema, - typeof AnnotationsRespondToolOutputSchema - > -{ +export class AnnotationsRespondTool implements McpToolDefinition< + typeof AnnotationsRespondToolInputSchema, + typeof AnnotationsRespondToolOutputSchema +> { name = MCP_TOOLS.ANNOTATION_RESPOND; description = "Store the agent's response to an annotation including explanation message and code patches. " + - 'Use after implementing changes to record what was done so users can review in the overlay.'; + 'Use after implementing changes to record what was done so users can review in the overlay. ' + + 'IMPORTANT: After calling this, you MUST call domscribe.annotation.updateStatus with status "processed" (or "failed") to complete the lifecycle.'; inputSchema = AnnotationsRespondToolInputSchema; outputSchema = AnnotationsRespondToolOutputSchema; @@ -57,6 +59,9 @@ export class AnnotationsRespondTool const output: AnnotationsRespondToolOutput = { success: response.success, annotationId: response.annotation.metadata.id, + nextStep: response.success + ? `Call domscribe.annotation.updateStatus with annotationId "${response.annotation.metadata.id}" and status "processed" to complete the lifecycle.` + : undefined, }; return { diff --git a/packages/domscribe-relay/src/mcp/tools/annotation-update-status.tool.ts b/packages/domscribe-relay/src/mcp/tools/annotation-update-status.tool.ts index 50351a3..69030f7 100644 --- a/packages/domscribe-relay/src/mcp/tools/annotation-update-status.tool.ts +++ b/packages/domscribe-relay/src/mcp/tools/annotation-update-status.tool.ts @@ -31,18 +31,15 @@ type AnnotationsUpdateStatusToolOutput = z.infer< typeof AnnotationsUpdateStatusToolOutputSchema >; -export class AnnotationsUpdateStatusTool - implements - McpToolDefinition< - typeof AnnotationsUpdateStatusToolInputSchema, - typeof AnnotationsUpdateStatusToolOutputSchema - > -{ +export class AnnotationsUpdateStatusTool implements McpToolDefinition< + typeof AnnotationsUpdateStatusToolInputSchema, + typeof AnnotationsUpdateStatusToolOutputSchema +> { name = MCP_TOOLS.ANNOTATION_UPDATE_STATUS; description = - "Update an annotation's status in its lifecycle. " + + 'Final step in the annotation lifecycle — call this AFTER domscribe.annotation.respond. ' + 'Valid transitions: queued→processing, processing→processed/failed, any→archived. ' + - 'Use to mark work as complete (processed), failed with error details, or archived.'; + 'Mark as "processed" when done, "failed" with errorDetails if unable to implement, or "archived" to remove from queue.'; inputSchema = AnnotationsUpdateStatusToolInputSchema; outputSchema = AnnotationsUpdateStatusToolOutputSchema; diff --git a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts index 26b7bc8..2411944 100644 --- a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts +++ b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts @@ -71,6 +71,7 @@ describe('QueryBySourceTool', () => { }, browserConnected: true, error: undefined, + hint: undefined, }); expect(JSON.parse(getResultText(result))).toEqual( result.structuredContent, @@ -100,6 +101,10 @@ describe('QueryBySourceTool', () => { runtime: undefined, browserConnected: undefined, error: undefined, + hint: + 'No manifest entry found for this source location. ' + + 'Try domscribe.manifest.query with the file path to discover which lines have entries, ' + + 'or use tolerance > 0 to widen the search.', }); }); @@ -129,6 +134,61 @@ describe('QueryBySourceTool', () => { }); }); + it('should return browser-not-connected hint when browserConnected is false', async () => { + // Arrange + const mockClient = createMockRelayClient({ + queryBySource: vi.fn().mockResolvedValue({ + found: true, + entryId: 'aB3dEf7h', + sourceLocation: { + file: 'src/components/Button.tsx', + start: { line: 10, column: 4 }, + }, + browserConnected: false, + }), + }); + const tool = new QueryBySourceTool(mockClient); + + // Act + const result: CallToolResult = await tool.toolCallback({ + file: 'src/components/Button.tsx', + line: 10, + }); + + // Assert + const structured = result.structuredContent as Record; + expect(structured['hint']).toContain('No browser is connected'); + expect(structured['hint']).toContain('Ask the user'); + }); + + it('should return not-rendered hint when element is not rendered', async () => { + // Arrange + const mockClient = createMockRelayClient({ + queryBySource: vi.fn().mockResolvedValue({ + found: true, + entryId: 'aB3dEf7h', + sourceLocation: { + file: 'src/components/Button.tsx', + start: { line: 10, column: 4 }, + }, + runtime: { rendered: false }, + browserConnected: true, + }), + }); + const tool = new QueryBySourceTool(mockClient); + + // Act + const result: CallToolResult = await tool.toolCallback({ + file: 'src/components/Button.tsx', + line: 10, + }); + + // Assert + const structured = result.structuredContent as Record; + expect(structured['hint']).toContain('not currently rendered'); + expect(structured['hint']).toContain('Ask the user to navigate'); + }); + it('should return MCP error result on exception', async () => { // Arrange const mockClient = createMockRelayClient({ @@ -155,7 +215,7 @@ describe('QueryBySourceTool', () => { const tool = new QueryBySourceTool(createMockRelayClient()); expect(tool.name).toBe(MCP_TOOLS.QUERY_BY_SOURCE); - expect(tool.description).toContain('source file'); + expect(tool.description).toContain('source location'); expect(tool.inputSchema).toBeDefined(); expect(tool.outputSchema).toBeDefined(); }); diff --git a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts index 1aae40c..122e10f 100644 --- a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts +++ b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts @@ -65,6 +65,10 @@ const QueryBySourceToolOutputSchema = McpToolOutputSchema.extend({ }) .optional(), browserConnected: z.boolean().optional(), + hint: z + .string() + .optional() + .describe('Actionable guidance based on the result'), }); type QueryBySourceToolOutput = z.infer; @@ -75,15 +79,43 @@ export class QueryBySourceTool implements McpToolDefinition< > { name = MCP_TOOLS.QUERY_BY_SOURCE; description = - "Query a live dev server by source file and position to get the element's " + - 'manifest entry and live runtime context (props, state, DOM info). ' + - 'Use when you have a source location and want to understand what the element ' + - 'looks like at runtime. Returns manifest data even if the browser is not connected.'; + 'Get live DOM snapshot, component props, and state for a source location (file + line). ' + + 'Call this BEFORE editing a UI component to understand its current rendered state, and AFTER editing to verify changes — ' + + 'replaces the need for curl, Playwright, or browser screenshots. ' + + 'If browserConnected is false, ask the user to navigate to the page in their browser and retry. ' + + 'Returns manifest data even when the browser is not connected.'; inputSchema = QueryBySourceToolInputSchema; outputSchema = QueryBySourceToolOutputSchema; constructor(private readonly relayHttpClient: RelayHttpClient) {} + private buildHint(result: { + found: boolean; + browserConnected?: boolean; + runtime?: { rendered?: boolean }; + }): string | undefined { + if (!result.found) { + return ( + 'No manifest entry found for this source location. ' + + 'Try domscribe.manifest.query with the file path to discover which lines have entries, ' + + 'or use tolerance > 0 to widen the search.' + ); + } + if (result.browserConnected === false) { + return ( + 'No browser is connected — runtime data is unavailable. ' + + 'Ask the user to open the page containing this component in their browser, then retry to get live props, state, and DOM snapshot.' + ); + } + if (result.runtime && !result.runtime.rendered) { + return ( + 'The element exists in the manifest but is not currently rendered in the browser. ' + + 'Ask the user to navigate to a page that renders this component, then retry.' + ); + } + return undefined; + } + async toolCallback(input: QueryBySourceToolInput) { try { const result = await this.relayHttpClient.queryBySource(input); @@ -95,6 +127,7 @@ export class QueryBySourceTool implements McpToolDefinition< runtime: result.runtime, browserConnected: result.browserConnected, error: result.error, + hint: this.buildHint(result), }; return { diff --git a/packages/domscribe-relay/src/plugins/kiro/POWER.md b/packages/domscribe-relay/src/plugins/kiro/POWER.md index 912dd75..4c1b4ab 100644 --- a/packages/domscribe-relay/src/plugins/kiro/POWER.md +++ b/packages/domscribe-relay/src/plugins/kiro/POWER.md @@ -1,7 +1,7 @@ --- name: 'domscribe' displayName: 'Domscribe' -description: 'Work with Domscribe — the pixel-to-code bridge that maps running UI elements to their exact source locations, capturing runtime context (props, state, DOM) for handoff to coding agents. Use when implementing features from captured UI annotations, querying runtime context for source locations, exploring component structure, or when user mentions annotations, queued tasks, UI changes, or asks about how elements render at runtime.' +description: 'Work with Domscribe — the pixel-to-code bridge that maps running UI elements to their exact source locations, capturing runtime context (props, state, DOM) for handoff to coding agents. Use when editing or modifying UI components (React, Vue, Next.js, Nuxt), implementing features from captured UI annotations, querying runtime context for source locations, exploring component structure, or when user mentions annotations, queued tasks, UI changes, props, state, DOM, or asks about how elements render at runtime.' keywords: [ 'domscribe', @@ -31,6 +31,20 @@ Domscribe bridges running UI and source code. It maps every rendered element to - **UI → Code**: User clicks an element in the browser, Domscribe captures it as an annotation with source location, runtime context, and user intent. You claim and implement it. - **Code → UI**: You're editing a source file and want to know what an element looks like at runtime. Query by file and line to get live props, state, and DOM snapshot. +## Editing Components (Code → UI) + +**Whenever you are about to edit a UI component**, use `domscribe.query.bySource` to see its live rendered state before making changes. This gives you the current DOM snapshot, component props, and state — no need for curl, Playwright, or browser screenshots. + +**Workflow:** + +1. **Before editing**: Call `domscribe.query.bySource` with the file path and line number of the element you're about to change. Inspect `runtime.componentProps`, `runtime.componentState`, and `runtime.domSnapshot` to understand the current state. +2. **Edit** the component source code. +3. **After editing**: Call `domscribe.query.bySource` again on the same location to verify your changes took effect in the live browser (HMR will have updated the page). + +**When the browser is not connected** (`browserConnected: false`): Ask the user to open the page containing this component in their browser, then retry. You will still receive manifest data (source location, component name), but live runtime data requires a browser connection. + +**When the element is not rendered** (`runtime.rendered: false`): The component exists in the codebase but the user hasn't navigated to a page that renders it. Ask them to navigate there and retry. + ## Quick Commands (MCP Prompts) | Command | Purpose | @@ -124,9 +138,11 @@ queued -> processing -> processed 4. **Implement** the change based on `userIntent` -5. **Store** your response via `domscribe.annotation.respond` with the annotation ID and a message describing what you did +5. **Verify** via `domscribe.query.bySource` — call with the same file and line to confirm your changes are reflected in the live browser (HMR will have updated the page) + +6. **Store** your response via `domscribe.annotation.respond` with the annotation ID and a message describing what you did -6. **Complete** the annotation via `domscribe.annotation.updateStatus` with status `processed` (or `failed` with `errorDetails`) +7. **Complete** the annotation via `domscribe.annotation.updateStatus` with status `processed` (or `failed` with `errorDetails`) ## Error Handling diff --git a/packages/domscribe-relay/src/skills/domscribe/SKILL.md b/packages/domscribe-relay/src/skills/domscribe/SKILL.md index a39896d..98f2423 100644 --- a/packages/domscribe-relay/src/skills/domscribe/SKILL.md +++ b/packages/domscribe-relay/src/skills/domscribe/SKILL.md @@ -1,6 +1,6 @@ --- name: domscribe -description: Work with Domscribe — the pixel-to-code bridge. Use when implementing features from captured UI annotations, querying runtime context for source locations, exploring component structure, or when user mentions annotations, queued tasks, UI changes, or asks about how elements render at runtime. +description: Work with Domscribe — the pixel-to-code bridge. Use when editing or modifying UI components (React, Vue, Next.js, Nuxt), implementing features from captured UI annotations, querying runtime context for source locations, exploring component structure, or when user mentions annotations, queued tasks, UI changes, props, state, DOM, or asks about how elements render at runtime. allowed-tools: Read, Edit, Write, mcp__domscribe__*, domscribe.*, mcp.domscribe.*, process_next, check_status, explore_component, find_annotations --- @@ -11,10 +11,24 @@ Domscribe bridges running UI and source code. It maps every rendered element to - **UI → Code**: User clicks an element in the browser, Domscribe captures it as an annotation with source location, runtime context, and user intent. You claim and implement it. - **Code → UI**: You're editing a source file and want to know what an element looks like at runtime. Query by file and line to get live props, state, and DOM snapshot. +## Editing Components (Code → UI) + +**Whenever you are about to edit a UI component**, use `domscribe.query.bySource` to see its live rendered state before making changes. This gives you the current DOM snapshot, component props, and state — no need for curl, Playwright, or browser screenshots. + +**Workflow:** + +1. **Before editing**: Call `domscribe.query.bySource` with the file path and line number of the element you're about to change. Inspect `runtime.componentProps`, `runtime.componentState`, and `runtime.domSnapshot` to understand the current state. +2. **Edit** the component source code. +3. **After editing**: Call `domscribe.query.bySource` again on the same location to verify your changes took effect in the live browser (HMR will have updated the page). + +**When the browser is not connected** (`browserConnected: false`): Ask the user to open the page containing this component in their browser, then retry. You will still receive manifest data (source location, component name), but live runtime data requires a browser connection. + +**When the element is not rendered** (`runtime.rendered: false`): The component exists in the codebase but the user hasn't navigated to a page that renders it. Ask them to navigate there and retry. + ## Quick Commands (MCP Prompts) -| Command | Purpose | -| ------------------------------------ | ------------------------------ | +| Command | Purpose | +| ------------------- | ------------------------------ | | `process_next` | Process next queued annotation | | `check_status` | System health and queue counts | | `explore_component` | List elements in a component | @@ -106,9 +120,11 @@ queued → processing → processed 4. **Implement** the change based on `userIntent` -5. **Store** your response via `domscribe.annotation.respond` with the annotation ID and a message describing what you did +5. **Verify** via `domscribe.query.bySource` — call with the same file and line to confirm your changes are reflected in the live browser (HMR will have updated the page) + +6. **Store** your response via `domscribe.annotation.respond` with the annotation ID and a message describing what you did -6. **Complete** the annotation via `domscribe.annotation.updateStatus` with status `processed` (or `failed` with `errorDetails`) +7. **Complete** the annotation via `domscribe.annotation.updateStatus` with status `processed` (or `failed` with `errorDetails`) ## Error Handling