@gemini-designer/mcp-server 0.1.2 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/components/catalog.d.ts.map +1 -1
  2. package/dist/components/catalog.js +10 -4
  3. package/dist/components/catalog.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +11 -6
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/context/builder.d.ts.map +1 -1
  8. package/dist/context/builder.js.map +1 -1
  9. package/dist/context/filter.d.ts.map +1 -1
  10. package/dist/context/filter.js +5 -1
  11. package/dist/context/filter.js.map +1 -1
  12. package/dist/context/grounding.d.ts.map +1 -1
  13. package/dist/context/grounding.js +7 -3
  14. package/dist/context/grounding.js.map +1 -1
  15. package/dist/context/guards.d.ts.map +1 -1
  16. package/dist/context/guards.js +53 -0
  17. package/dist/context/guards.js.map +1 -1
  18. package/dist/context/repo-hints.js.map +1 -1
  19. package/dist/context/styling-detector.d.ts +24 -0
  20. package/dist/context/styling-detector.d.ts.map +1 -0
  21. package/dist/context/styling-detector.js +337 -0
  22. package/dist/context/styling-detector.js.map +1 -0
  23. package/dist/design/principles.js.map +1 -1
  24. package/dist/generation/gemini-client.d.ts.map +1 -1
  25. package/dist/generation/gemini-client.js.map +1 -1
  26. package/dist/generation/litellm-client.d.ts.map +1 -1
  27. package/dist/generation/litellm-client.js +14 -7
  28. package/dist/generation/litellm-client.js.map +1 -1
  29. package/dist/generation/remote-client.d.ts +10 -5
  30. package/dist/generation/remote-client.d.ts.map +1 -1
  31. package/dist/generation/remote-client.js +13 -2
  32. package/dist/generation/remote-client.js.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/output/file-writer.d.ts.map +1 -1
  35. package/dist/output/file-writer.js +4 -4
  36. package/dist/output/file-writer.js.map +1 -1
  37. package/dist/output/formatter.d.ts.map +1 -1
  38. package/dist/output/formatter.js +5 -2
  39. package/dist/output/formatter.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +2 -1
  42. package/dist/server.js.map +1 -1
  43. package/dist/stack/detect.d.ts.map +1 -1
  44. package/dist/stack/detect.js +42 -9
  45. package/dist/stack/detect.js.map +1 -1
  46. package/dist/tokens/sync.d.ts.map +1 -1
  47. package/dist/tokens/sync.js +22 -5
  48. package/dist/tokens/sync.js.map +1 -1
  49. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
  50. package/dist/tools/analyze-screenshot-ui.js +5 -5
  51. package/dist/tools/analyze-screenshot-ui.js.map +1 -1
  52. package/dist/tools/analyze-tokens.d.ts.map +1 -1
  53. package/dist/tools/analyze-tokens.js +3 -1
  54. package/dist/tools/analyze-tokens.js.map +1 -1
  55. package/dist/tools/catalog-components.d.ts.map +1 -1
  56. package/dist/tools/catalog-components.js +1 -4
  57. package/dist/tools/catalog-components.js.map +1 -1
  58. package/dist/tools/create-ui.d.ts +3 -0
  59. package/dist/tools/create-ui.d.ts.map +1 -1
  60. package/dist/tools/create-ui.js +203 -75
  61. package/dist/tools/create-ui.js.map +1 -1
  62. package/dist/tools/detect-ui-stack.js.map +1 -1
  63. package/dist/tools/generate-component-variants.d.ts.map +1 -1
  64. package/dist/tools/generate-component-variants.js +15 -4
  65. package/dist/tools/generate-component-variants.js.map +1 -1
  66. package/dist/tools/generate-vibes.d.ts.map +1 -1
  67. package/dist/tools/generate-vibes.js +7 -3
  68. package/dist/tools/generate-vibes.js.map +1 -1
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/modify-ui.d.ts.map +1 -1
  71. package/dist/tools/modify-ui.js +7 -2
  72. package/dist/tools/modify-ui.js.map +1 -1
  73. package/dist/tools/scaffold-project.d.ts.map +1 -1
  74. package/dist/tools/scaffold-project.js +3 -1
  75. package/dist/tools/scaffold-project.js.map +1 -1
  76. package/dist/tools/snippet-ui.d.ts +3 -1
  77. package/dist/tools/snippet-ui.d.ts.map +1 -1
  78. package/dist/tools/snippet-ui.js +219 -88
  79. package/dist/tools/snippet-ui.js.map +1 -1
  80. package/dist/tools/sync-design-tokens.d.ts.map +1 -1
  81. package/dist/tools/sync-design-tokens.js +26 -11
  82. package/dist/tools/sync-design-tokens.js.map +1 -1
  83. package/dist/utils/walk.d.ts.map +1 -1
  84. package/dist/utils/walk.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +5 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +55 -55
  90. package/src/__tests__/builder.test.ts +19 -19
  91. package/src/__tests__/config.test.ts +63 -31
  92. package/src/__tests__/filter.test.ts +98 -92
  93. package/src/__tests__/remote-client.test.ts +179 -0
  94. package/src/components/catalog.ts +170 -166
  95. package/src/config/index.ts +185 -177
  96. package/src/context/builder.ts +157 -157
  97. package/src/context/filter.ts +110 -104
  98. package/src/context/grounding.ts +143 -129
  99. package/src/context/guards.ts +97 -38
  100. package/src/context/repo-hints.ts +24 -24
  101. package/src/context/styling-detector.ts +460 -0
  102. package/src/design/principles.ts +14 -14
  103. package/src/generation/gemini-client.ts +53 -56
  104. package/src/generation/litellm-client.ts +102 -86
  105. package/src/generation/remote-client.ts +100 -77
  106. package/src/index.ts +16 -16
  107. package/src/output/file-writer.ts +123 -123
  108. package/src/output/formatter.ts +139 -132
  109. package/src/server.ts +12 -11
  110. package/src/stack/detect.ts +226 -175
  111. package/src/tokens/sync.ts +189 -155
  112. package/src/tools/analyze-screenshot-ui.ts +89 -88
  113. package/src/tools/analyze-tokens.ts +80 -78
  114. package/src/tools/catalog-components.ts +68 -68
  115. package/src/tools/create-ui.ts +295 -142
  116. package/src/tools/detect-ui-stack.ts +36 -36
  117. package/src/tools/generate-component-variants.ts +155 -135
  118. package/src/tools/generate-vibes.ts +121 -117
  119. package/src/tools/index.ts +14 -14
  120. package/src/tools/modify-ui.ts +170 -165
  121. package/src/tools/scaffold-project.ts +68 -66
  122. package/src/tools/snippet-ui.ts +323 -172
  123. package/src/tools/sync-design-tokens.ts +217 -195
  124. package/src/utils/walk.ts +47 -45
  125. package/src/version.ts +6 -0
  126. package/tsconfig.json +23 -33
  127. package/vitest.config.ts +10 -10
  128. package/.prettierrc +0 -9
  129. package/eslint.config.js +0 -37
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * Generate standalone UI components (modals, tables, forms, etc.)
5
5
  * for injection into an existing architecture.
6
- * Can optionally write directly to a file if requested.
6
+ *
7
+ * Auto-detects project styling approach (Tailwind, CSS Modules, styled-components, etc.)
8
+ * and generates matching code structure.
7
9
  */
8
10
 
9
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -14,215 +16,364 @@ import { buildContext } from '../context/builder.js';
14
16
  import { buildRepoHints } from '../context/repo-hints.js';
15
17
  import { assertWritablePath } from '../context/guards.js';
16
18
  import { writeFile } from '../output/file-writer.js';
19
+ import { formatCode } from '../output/formatter.js';
17
20
  import { DESIGN_PRINCIPLES_COMPACT } from '../design/principles.js';
21
+ import {
22
+ detectStylingApproach,
23
+ getStylingInstructions,
24
+ StylingApproach,
25
+ StylingInfo,
26
+ } from '../context/styling-detector.js';
27
+
28
+ interface GeneratedFile {
29
+ path: string;
30
+ content: string;
31
+ type: 'component' | 'styles' | 'types';
32
+ }
18
33
 
19
34
  const inputSchema = {
20
- type: z
21
- .enum(['modal', 'table', 'chart', 'form', 'card', 'nav', 'hero', 'footer', 'sidebar', 'custom'])
22
- .describe('Type of UI component to generate'),
23
- description: z
24
- .string()
25
- .describe(
26
- 'Detailed description of the component (e.g., "A data table with sorting, filtering, and pagination")'
27
- ),
28
- framework: z
29
- .enum(['vanilla', 'react', 'vue', 'svelte', 'nextjs'])
30
- .optional()
31
- .describe('Target framework'),
32
- context: z
33
- .array(z.string())
34
- .optional()
35
- .describe('Paths to design tokens or existing components to match style'),
36
- propsInterface: z
37
- .string()
38
- .optional()
39
- .describe('TypeScript interface for component props (if applicable)'),
40
- animated: z.boolean().default(false).describe('Include animations/transitions'),
41
- targetPath: z.string().optional().describe('Where to write the output file (if writeToFile=true)'),
42
- writeToFile: z.boolean().default(false).describe('If true, write the generated code to targetPath'),
35
+ type: z
36
+ .enum(['modal', 'table', 'chart', 'form', 'card', 'nav', 'hero', 'footer', 'sidebar', 'custom'])
37
+ .describe('Type of UI component to generate'),
38
+ description: z
39
+ .string()
40
+ .describe(
41
+ 'Detailed description of the component (e.g., "A data table with sorting, filtering, and pagination")'
42
+ ),
43
+ framework: z
44
+ .enum(['vanilla', 'react', 'vue', 'svelte', 'nextjs'])
45
+ .optional()
46
+ .describe('Target framework'),
47
+ targetPath: z.string().describe('Target file path for component (e.g. src/components/Modal.tsx)'),
48
+ projectRoot: z.string().optional().describe('Project root for auto-detecting styling approach'),
49
+ stylingApproach: z
50
+ .enum([
51
+ 'auto',
52
+ 'tailwind',
53
+ 'css-modules',
54
+ 'styled-components',
55
+ 'emotion',
56
+ 'scss',
57
+ 'vanilla-extract',
58
+ 'panda-css',
59
+ 'uno-css',
60
+ 'stylex',
61
+ 'vanilla-css',
62
+ ])
63
+ .default('auto')
64
+ .describe('Styling approach (auto-detects from project if "auto")'),
65
+ context: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe('Paths to design tokens or existing components to match style'),
69
+ propsInterface: z
70
+ .string()
71
+ .optional()
72
+ .describe('TypeScript interface for component props (if applicable)'),
73
+ animated: z.boolean().default(false).describe('Include animations/transitions'),
74
+ writeToFile: z.boolean().default(false).describe('Write generated files to disk'),
75
+ backup: z.boolean().default(true).describe('Create .bak backup if overwriting'),
76
+ format: z.boolean().default(true).describe('Format code with Prettier'),
43
77
  };
44
78
 
45
- function getSystemPrompt(type: string, framework: string): string {
46
- return `You are a senior frontend engineer and designer creating DISTINCTIVE, memorable UI.
79
+ function getSystemPrompt(type: string, framework: string, stylingInfo: StylingInfo): string {
80
+ const needsSeparateStyleFile = ![
81
+ 'tailwind',
82
+ 'panda-css',
83
+ 'uno-css',
84
+ 'styled-components',
85
+ 'emotion',
86
+ ].includes(stylingInfo.approach);
87
+
88
+ return `You are a senior frontend engineer creating DISTINCTIVE, memorable UI components.
47
89
 
48
90
  Generate a standalone ${type} component in ${framework} that avoids generic AI aesthetics.
49
91
 
50
92
  ${DESIGN_PRINCIPLES_COMPACT}
51
93
 
94
+ ${getStylingInstructions(stylingInfo)}
95
+
96
+ ## Output Structure (CRITICAL)
97
+
98
+ You MUST return a valid JSON object:
99
+ {
100
+ "files": [
101
+ {
102
+ "path": "<componentPath>",
103
+ "content": "<component code>",
104
+ "type": "component"
105
+ }${
106
+ needsSeparateStyleFile
107
+ ? `,
108
+ {
109
+ "path": "<stylePath>",
110
+ "content": "<styles>",
111
+ "type": "styles"
112
+ }`
113
+ : ''
114
+ }
115
+ ]
116
+ }
117
+
52
118
  Hard rules:
53
- - Output ONLY the code. No explanations. No markdown. No code fences.
54
- - Accessible by default (WCAG AA): semantic HTML, labels, aria-* where needed, keyboard support, focus-visible.
119
+ - Output ONLY valid JSON. No markdown. No code fences. No explanations.
120
+ - Accessible by default (WCAG AA): semantic HTML, labels, aria-*, keyboard support, focus-visible.
55
121
  - Responsive by default (mobile-first).
56
- - Do not add new dependencies unless explicitly requested.
57
122
  - Avoid placeholder TODOs. Provide complete, production-ready implementation.
58
- - Use distinctive typography (avoid Inter, Roboto, Arial).
59
- - Create visual depth with backgrounds (gradients, textures, shadows).
60
- - Include purposeful animations for polish.
123
+ - Use the detected styling approach correctly.
61
124
 
62
125
  Type-specific requirements:
63
126
  ${getComponentGuidelines(type)}`;
64
127
  }
65
128
 
66
129
  function getComponentGuidelines(type: string): string {
67
- const guidelines: Record<string, string> = {
68
- modal: [
69
- '- Trap focus inside the modal',
70
- '- Close on Escape',
71
- '- Close on backdrop click',
72
- '- Ensure aria-modal, role=dialog, accessible title/description wiring',
73
- ].join('\n'),
74
- table: [
75
- '- Optional sortable columns (no external deps)',
76
- '- Responsive: horizontal scroll or stacked layout on small screens',
77
- '- Accessible headers and caption support',
78
- ].join('\n'),
79
- form: [
80
- '- Accessible labels and error messages',
81
- '- Validation states and disabled/loading submit state',
82
- '- Keyboard submit behavior',
83
- ].join('\n'),
84
- card: ['- Flexible content slots', '- Hover/focus states', '- Optional media support'].join('\n'),
85
- nav: [
86
- '- Mobile hamburger pattern if needed',
87
- '- Active state indication',
88
- '- Keyboard navigation (tab, arrow keys if menus)',
89
- ].join('\n'),
90
- hero: ['- Responsive layout', '- CTA buttons', '- Ensure text contrast/legibility'].join('\n'),
91
- footer: ['- Responsive multi-column layout', '- Social links', '- Legal/copyright'].join('\n'),
92
- sidebar: ['- Collapsible (optional)', '- Active item highlighting', '- Nested items support'].join('\n'),
93
- chart: [
94
- '- Responsive sizing',
95
- '- Provide an accessible table fallback for data',
96
- '- Avoid heavy chart deps unless explicitly requested',
97
- ].join('\n'),
98
- custom: '- Follow best practices for the described component type',
99
- };
100
-
101
- return guidelines[type] || guidelines.custom;
130
+ const guidelines: Record<string, string> = {
131
+ modal: [
132
+ '- Trap focus inside the modal',
133
+ '- Close on Escape',
134
+ '- Close on backdrop click',
135
+ '- Ensure aria-modal, role=dialog, accessible title/description wiring',
136
+ ].join('\n'),
137
+ table: [
138
+ '- Optional sortable columns (no external deps)',
139
+ '- Responsive: horizontal scroll or stacked layout on small screens',
140
+ '- Accessible headers and caption support',
141
+ ].join('\n'),
142
+ form: [
143
+ '- Accessible labels and error messages',
144
+ '- Validation states and disabled/loading submit state',
145
+ '- Keyboard submit behavior',
146
+ ].join('\n'),
147
+ card: ['- Flexible content slots', '- Hover/focus states', '- Optional media support'].join(
148
+ '\n'
149
+ ),
150
+ nav: [
151
+ '- Mobile hamburger pattern if needed',
152
+ '- Active state indication',
153
+ '- Keyboard navigation (tab, arrow keys if menus)',
154
+ ].join('\n'),
155
+ hero: ['- Responsive layout', '- CTA buttons', '- Ensure text contrast/legibility'].join('\n'),
156
+ footer: ['- Responsive multi-column layout', '- Social links', '- Legal/copyright'].join('\n'),
157
+ sidebar: [
158
+ '- Collapsible (optional)',
159
+ '- Active item highlighting',
160
+ '- Nested items support',
161
+ ].join('\n'),
162
+ chart: [
163
+ '- Responsive sizing',
164
+ '- Provide an accessible table fallback for data',
165
+ '- Avoid heavy chart deps unless explicitly requested',
166
+ ].join('\n'),
167
+ custom: '- Follow best practices for the described component type',
168
+ };
169
+
170
+ return guidelines[type] || guidelines.custom;
102
171
  }
103
172
 
104
173
  /**
105
- * Extract code from markdown code blocks.
106
- * (We still instruct the model not to use fences; this is a safety net.)
174
+ * Extract JSON from model response
107
175
  */
108
- function extractCode(response: string): string {
109
- const codeBlockMatch = response.match(/```(?:\w+)?\n([\s\S]*?)```/);
110
- if (codeBlockMatch) {
111
- return codeBlockMatch[1].trim();
112
- }
113
- return response.trim();
176
+ function extractJson(response: string): string {
177
+ const jsonBlockMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
178
+ if (jsonBlockMatch) {
179
+ return jsonBlockMatch[1].trim();
180
+ }
181
+ const jsonMatch = response.match(/\{[\s\S]*"files"[\s\S]*\}/);
182
+ if (jsonMatch) {
183
+ return jsonMatch[0];
184
+ }
185
+ return response.trim();
186
+ }
187
+
188
+ /**
189
+ * Derive style file path from component path
190
+ */
191
+ function deriveStylePath(componentPath: string, stylingInfo: StylingInfo): string {
192
+ const ext = stylingInfo.fileExtension;
193
+ if (!ext) return '';
194
+ return componentPath.replace(/\.(tsx?|jsx?|vue|svelte)$/, ext);
114
195
  }
115
196
 
116
197
  export function registerSnippetUI(server: McpServer, config: Config): void {
117
- server.registerTool(
118
- 'snippet_ui',
119
- {
120
- title: 'Generate UI Snippet',
121
- description:
122
- 'Generate standalone UI components (modals, tables, forms, etc.) ready to inject. Set writeToFile=true to write directly.',
123
- inputSchema,
124
- },
125
- async (args) => {
126
- const type = args.type as string;
127
- const description = args.description as string;
128
- const framework =
129
- (args.framework as 'vanilla' | 'react' | 'vue' | 'svelte' | 'nextjs') ||
130
- config.defaultFramework;
131
- const contextPaths = args.context as string[] | undefined;
132
- const propsInterface = args.propsInterface as string | undefined;
133
- const animated = args.animated === true;
134
- const targetPath = args.targetPath as string | undefined;
135
- const shouldWrite = args.writeToFile === true;
136
-
137
- // Build context from existing files
138
- let contextContent = '';
139
- if (contextPaths && contextPaths.length > 0) {
140
- contextContent = await buildContext(contextPaths, config);
141
- }
198
+ server.registerTool(
199
+ 'snippet_ui',
200
+ {
201
+ title: 'Generate UI Snippet',
202
+ description:
203
+ 'Generate standalone UI components (modals, tables, forms, etc.). Auto-detects project styling and outputs matching code structure.',
204
+ inputSchema,
205
+ },
206
+ async (args) => {
207
+ try {
208
+ const type = args.type as string;
209
+ const description = args.description as string;
210
+ const framework =
211
+ (args.framework as 'vanilla' | 'react' | 'vue' | 'svelte' | 'nextjs') ||
212
+ config.defaultFramework;
213
+ const targetPath = args.targetPath as string;
214
+ const projectRoot = (args.projectRoot as string) || process.cwd();
215
+ const requestedApproach = (args.stylingApproach as string) || 'auto';
216
+ const contextPaths = args.context as string[] | undefined;
217
+ const propsInterface = args.propsInterface as string | undefined;
218
+ const animated = args.animated === true;
219
+ const shouldWrite = args.writeToFile === true;
220
+ const backup = args.backup !== false;
221
+ const format = args.format !== false;
142
222
 
143
- const systemPrompt = getSystemPrompt(type, framework);
223
+ // Detect or use specified styling approach
224
+ let stylingInfo: StylingInfo;
225
+ if (requestedApproach === 'auto') {
226
+ stylingInfo = detectStylingApproach(projectRoot);
227
+ if (config.debug) {
228
+ console.error(`[snippet_ui] Auto-detected styling: ${stylingInfo.approach}`);
229
+ }
230
+ } else {
231
+ stylingInfo = {
232
+ approach: requestedApproach as StylingApproach,
233
+ confidence: 'high',
234
+ detectedFrom: 'user specified',
235
+ fileExtension: getFileExtension(requestedApproach as StylingApproach),
236
+ importStatement: '',
237
+ usage: '',
238
+ };
239
+ }
144
240
 
145
- let userPrompt = `Create a ${type} component.
241
+ // Build context from existing files
242
+ let contextContent = '';
243
+ if (contextPaths && contextPaths.length > 0) {
244
+ contextContent = await buildContext(contextPaths, config);
245
+ }
246
+
247
+ const stylePath = deriveStylePath(targetPath, stylingInfo);
248
+ const systemPrompt = getSystemPrompt(type, framework, stylingInfo);
249
+
250
+ let userPrompt = `Create a ${type} component using ${stylingInfo.approach.toUpperCase()} styling.
146
251
 
147
252
  ${buildRepoHints(config)}
148
253
 
254
+ Component file: ${targetPath}
255
+ ${stylePath ? `Style file: ${stylePath}` : ''}
256
+
149
257
  Description:
150
258
  ${description}`;
151
259
 
152
- if (propsInterface) {
153
- userPrompt += `\n\nProps interface:\n${propsInterface}`;
154
- }
260
+ if (propsInterface) {
261
+ userPrompt += `\n\nProps interface:\n${propsInterface}`;
262
+ }
155
263
 
156
- if (animated) {
157
- userPrompt += `\n\nInclude smooth, subtle animations/transitions (respect reduced motion).`;
158
- }
264
+ if (animated) {
265
+ userPrompt += `\n\nInclude smooth, subtle animations/transitions (respect reduced motion).`;
266
+ }
159
267
 
160
- if (contextContent) {
161
- userPrompt += `\n\nMatch the style/tokens from these existing files:\n${contextContent}`;
162
- }
268
+ if (contextContent) {
269
+ userPrompt += `\n\nMatch the style/tokens from these existing files:\n${contextContent}`;
270
+ }
271
+
272
+ userPrompt += `\n\nReturn a JSON object with "files" array.`;
273
+
274
+ const response = await generateWithGemini(config, systemPrompt, userPrompt, {
275
+ toolName: 'snippet_ui',
276
+ });
277
+ const jsonStr = extractJson(response);
163
278
 
279
+ let parsed: { files?: GeneratedFile[] };
280
+ try {
281
+ parsed = JSON.parse(jsonStr);
282
+ } catch {
283
+ return {
284
+ content: [
285
+ {
286
+ type: 'text' as const,
287
+ text: `Error: Model did not return valid JSON.\n${response}`,
288
+ },
289
+ ],
290
+ isError: true,
291
+ };
292
+ }
293
+
294
+ if (!parsed.files || !Array.isArray(parsed.files) || parsed.files.length === 0) {
295
+ return {
296
+ content: [{ type: 'text' as const, text: 'Error: Model output missing "files" array' }],
297
+ isError: true,
298
+ };
299
+ }
300
+
301
+ // Format files if requested
302
+ if (format) {
303
+ for (const file of parsed.files) {
164
304
  try {
165
- const response = await generateWithGemini(config, systemPrompt, userPrompt, { toolName: 'snippet_ui' });
166
- const code = extractCode(response);
167
-
168
- // Write to file if requested
169
- if (shouldWrite) {
170
- if (!targetPath) {
171
- return {
172
- content: [
173
- {
174
- type: 'text' as const,
175
- text: `Error: writeToFile=true but no targetPath was provided.`,
176
- },
177
- ],
178
- isError: true,
179
- };
180
- }
181
-
182
- const safePath = assertWritablePath(targetPath, config);
183
- const result = await writeFile(safePath, code, { format: true, backup: true });
184
-
185
- if (!result.success) {
186
- return {
187
- content: [
188
- {
189
- type: 'text' as const,
190
- text: `❌ Failed to write file: ${result.error || 'Unknown error'}`,
191
- },
192
- ],
193
- isError: true,
194
- };
195
- }
196
-
197
- const msg = [
198
- `✅ Snippet written successfully.`,
199
- `File: ${result.filePath}`,
200
- result.backupPath ? `Backup: ${result.backupPath}` : undefined,
201
- ]
202
- .filter(Boolean)
203
- .join('\n');
204
-
205
- return {
206
- content: [{ type: 'text' as const, text: msg }],
207
- };
208
- }
209
-
210
- // Return code only (no wrappers) for best agent compatibility
211
- return {
212
- content: [{ type: 'text' as const, text: code }],
213
- };
214
- } catch (error) {
215
- const message = error instanceof Error ? error.message : 'Unknown error';
216
- return {
217
- content: [
218
- {
219
- type: 'text' as const,
220
- text: `Error generating snippet: ${message}`,
221
- },
222
- ],
223
- isError: true,
224
- };
305
+ file.content = await formatCode(file.content, { filePath: file.path });
306
+ } catch {
307
+ // Ignore formatting errors
308
+ }
309
+ }
310
+ }
311
+
312
+ const result = {
313
+ stylingApproach: stylingInfo.approach,
314
+ detectedFrom: stylingInfo.detectedFrom,
315
+ confidence: stylingInfo.confidence,
316
+ files: parsed.files,
317
+ };
318
+
319
+ // Write to files if requested
320
+ if (shouldWrite) {
321
+ const writes: Array<{ file: string; backup?: string }> = [];
322
+
323
+ for (const file of parsed.files) {
324
+ const safePath = assertWritablePath(file.path, config);
325
+ const writeResult = await writeFile(safePath, file.content, { backup, format: false });
326
+
327
+ if (!writeResult.success) {
328
+ return {
329
+ content: [
330
+ {
331
+ type: 'text' as const,
332
+ text: `Error writing ${file.path}: ${writeResult.error}`,
333
+ },
334
+ ],
335
+ isError: true,
336
+ };
225
337
  }
338
+ writes.push({ file: writeResult.filePath, backup: writeResult.backupPath });
339
+ }
340
+
341
+ const msg = [
342
+ `✅ Snippet files written (${stylingInfo.approach} styling):`,
343
+ ...writes.map((w) => `- ${w.file}${w.backup ? ` (backup: ${w.backup})` : ''}`),
344
+ ].join('\n');
345
+
346
+ return { content: [{ type: 'text' as const, text: msg }] };
226
347
  }
227
- );
348
+
349
+ // Return JSON with files and metadata
350
+ return {
351
+ content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
352
+ };
353
+ } catch (error) {
354
+ const message = error instanceof Error ? error.message : 'Unknown error';
355
+ return {
356
+ content: [{ type: 'text' as const, text: `Error generating snippet: ${message}` }],
357
+ isError: true,
358
+ };
359
+ }
360
+ }
361
+ );
362
+ }
363
+
364
+ function getFileExtension(approach: StylingApproach): string {
365
+ const extensions: Record<StylingApproach, string> = {
366
+ tailwind: '',
367
+ 'css-modules': '.module.css',
368
+ 'styled-components': '.tsx',
369
+ emotion: '.tsx',
370
+ scss: '.module.scss',
371
+ 'vanilla-extract': '.css.ts',
372
+ 'panda-css': '',
373
+ 'uno-css': '',
374
+ stylex: '.stylex.ts',
375
+ 'css-in-js': '.tsx',
376
+ 'vanilla-css': '.css',
377
+ };
378
+ return extensions[approach] || '.css';
228
379
  }