Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,63 @@ describe('QueryBySourceTool', () => {
},
browserConnected: true,
error: undefined,
hint: undefined,
// The runtime is rendered but `componentStyles` is absent — the
// hint nudges the agent to enable `captureStyles` so the next
// styling query lands in one round trip (RFC 0001).
hint: expect.stringContaining('captureStyles'),
});
expect(JSON.parse(getResultText(result))).toEqual(
result.structuredContent,
);
});

it('emits no captureStyles hint when componentStyles is already present', async () => {
// Arrange — simulate the post-RFC 0001 happy path: runtime is configured
// with captureStyles, the query returns componentStyles, agent has full
// ground truth and needs no follow-on nudge.
const mockClient = createMockRelayClient({
queryBySource: vi.fn().mockResolvedValue({
found: true,
entryId: 'aB3dEf7h',
sourceLocation: {
file: 'src/components/Button.tsx',
start: { line: 10, column: 4 },
},
runtime: {
rendered: true,
componentProps: { label: 'Submit' },
componentStyles: {
computed: { padding: '16px', color: 'rgb(15, 23, 42)' },
customProperties: { '--color-fg': 'rgb(15, 23, 42)' },
},
},
browserConnected: true,
}),
});
const tool = new QueryBySourceTool(mockClient);

// Act
const result: CallToolResult = await tool.toolCallback({
file: 'src/components/Button.tsx',
line: 10,
});

// Assert
expect(
(result.structuredContent as { hint?: string }).hint,
).toBeUndefined();
expect(
(
result.structuredContent as {
runtime?: { componentStyles?: unknown };
}
).runtime?.componentStyles,
).toEqual({
computed: { padding: '16px', color: 'rgb(15, 23, 42)' },
customProperties: { '--color-fg': 'rgb(15, 23, 42)' },
});
});

it('should handle not-found responses', async () => {
// Arrange
const mockClient = createMockRelayClient({
Expand Down
42 changes: 39 additions & 3 deletions packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ const QueryBySourceToolOutputSchema = McpToolOutputSchema.extend({
rendered: z.boolean(),
componentProps: z.unknown().optional(),
componentState: z.unknown().optional(),
componentStyles: z
.object({
computed: z
.record(z.string(), z.string())
.optional()
.describe(
'Computed CSS properties from the allowlist (display, padding, color, font-*, etc.). Use this to confirm what the element actually renders before editing.',
),
customProperties: z
.record(z.string(), z.string())
.optional()
.describe(
"Resolved `--*` CSS custom properties visible at the element. When a computed value matches a token's resolved value, prefer the token name in the fix.",
),
})
.optional()
.describe(
'Runtime style snapshot (RFC 0001). Present only when the runtime is configured with `captureStyles: true`. Use this on styling annotations to ground the agent edit in what the user sees, not just what the source says.',
),
domSnapshot: z
.object({
tagName: z.string().optional(),
Expand All @@ -79,11 +98,13 @@ export class QueryBySourceTool implements McpToolDefinition<
> {
name = MCP_TOOLS.QUERY_BY_SOURCE;
description =
'Get live DOM snapshot, component props, and state for a source location (file + line). ' +
'Get live DOM snapshot, component props, state, and computed styles for a source location (file + line). ' +
'Call this when fixing visual/styling bugs, debugging conditional rendering, tracing prop values, or verifying UI changes after editing. ' +
'For styling annotations specifically — padding, color, spacing, typography, layout — always call this first: `runtime.componentStyles.computed` is the ground truth for what the user sees, and `runtime.componentStyles.customProperties` exposes the resolved design-system tokens (`--*` vars) at the element. Prefer token names over raw values in your edit when the resolved value matches. ' +
"Skip for pure logic changes, new files, refactoring, or type fixes — runtime context won't help there. " +
'If browserConnected is false, ask the user to open the page in their browser and retry. ' +
'Returns manifest data even when the browser is not connected.';
'Returns manifest data even when the browser is not connected. ' +
'NOTE: `componentStyles` requires the runtime to be configured with `captureStyles: true` (off by default in v0.x for payload-size reasons).';
inputSchema = QueryBySourceToolInputSchema;
outputSchema = QueryBySourceToolOutputSchema;

Expand All @@ -92,7 +113,10 @@ export class QueryBySourceTool implements McpToolDefinition<
private buildHint(result: {
found: boolean;
browserConnected?: boolean;
runtime?: { rendered?: boolean };
runtime?: {
rendered?: boolean;
componentStyles?: unknown;
};
}): string | undefined {
if (!result.found) {
return (
Expand All @@ -113,6 +137,18 @@ export class QueryBySourceTool implements McpToolDefinition<
'Ask the user to navigate to a page that renders this component, then retry.'
);
}
if (
result.runtime &&
result.runtime.rendered &&
!result.runtime.componentStyles
) {
return (
'componentStyles is not in this response. If the user is asking about a styling change ' +
'(padding, color, spacing, typography, layout), ask the user to set `captureStyles: true` ' +
"in their Domscribe runtime config and retry — you'll get the computed-style + token snapshot " +
'for the element, which lets the edit land in one round trip.'
);
}
return undefined;
}

Expand Down
11 changes: 11 additions & 0 deletions packages/domscribe-relay/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ export const QueryBySourceResponseSchema = z.object({
rendered: z.boolean(),
componentProps: z.unknown().optional(),
componentState: z.unknown().optional(),
componentStyles: z
.object({
computed: z.record(z.string(), z.string()).optional(),
customProperties: z.record(z.string(), z.string()).optional(),
})
.optional()
.describe(
'Runtime computed styles + resolved CSS custom properties (RFC 0001). ' +
'Populated when the runtime is configured with `captureStyles: true`. ' +
'Use this to verify what the user actually sees before editing styling source.',
),
domSnapshot: z
.object({
tagName: z.string().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class QueryBySourceRoute implements RelayRoute {
rendered: wsResult.rendered ?? false,
componentProps: wsResult.context?.componentProps,
componentState: wsResult.context?.componentState,
componentStyles: wsResult.context?.componentStyles,
domSnapshot: wsResult.elementInfo,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`StyleCapturer > allowlist invariants > orders entries deterministically (snapshot for CI stability) 1`] = `
[
"display",
"position",
"box-sizing",
"margin",
"padding",
"width",
"height",
"top",
"right",
"bottom",
"left",
"z-index",
"color",
"background-color",
"background-image",
"font-family",
"font-size",
"font-weight",
"line-height",
"letter-spacing",
"text-align",
"border",
"border-radius",
"box-shadow",
"opacity",
"transform",
"transition",
"cursor",
"flex",
"gap",
"grid-template-columns",
"overflow",
]
`;
Loading
Loading