@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.
- package/dist/components/catalog.d.ts.map +1 -1
- package/dist/components/catalog.js +10 -4
- package/dist/components/catalog.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -6
- package/dist/config/index.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/filter.d.ts.map +1 -1
- package/dist/context/filter.js +5 -1
- package/dist/context/filter.js.map +1 -1
- package/dist/context/grounding.d.ts.map +1 -1
- package/dist/context/grounding.js +7 -3
- package/dist/context/grounding.js.map +1 -1
- package/dist/context/guards.d.ts.map +1 -1
- package/dist/context/guards.js +53 -0
- package/dist/context/guards.js.map +1 -1
- package/dist/context/repo-hints.js.map +1 -1
- package/dist/context/styling-detector.d.ts +24 -0
- package/dist/context/styling-detector.d.ts.map +1 -0
- package/dist/context/styling-detector.js +337 -0
- package/dist/context/styling-detector.js.map +1 -0
- package/dist/design/principles.js.map +1 -1
- package/dist/generation/gemini-client.d.ts.map +1 -1
- package/dist/generation/gemini-client.js.map +1 -1
- package/dist/generation/litellm-client.d.ts.map +1 -1
- package/dist/generation/litellm-client.js +14 -7
- package/dist/generation/litellm-client.js.map +1 -1
- package/dist/generation/remote-client.d.ts +10 -5
- package/dist/generation/remote-client.d.ts.map +1 -1
- package/dist/generation/remote-client.js +13 -2
- package/dist/generation/remote-client.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output/file-writer.d.ts.map +1 -1
- package/dist/output/file-writer.js +4 -4
- package/dist/output/file-writer.js.map +1 -1
- package/dist/output/formatter.d.ts.map +1 -1
- package/dist/output/formatter.js +5 -2
- package/dist/output/formatter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/stack/detect.d.ts.map +1 -1
- package/dist/stack/detect.js +42 -9
- package/dist/stack/detect.js.map +1 -1
- package/dist/tokens/sync.d.ts.map +1 -1
- package/dist/tokens/sync.js +22 -5
- package/dist/tokens/sync.js.map +1 -1
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
- package/dist/tools/analyze-screenshot-ui.js +5 -5
- package/dist/tools/analyze-screenshot-ui.js.map +1 -1
- package/dist/tools/analyze-tokens.d.ts.map +1 -1
- package/dist/tools/analyze-tokens.js +3 -1
- package/dist/tools/analyze-tokens.js.map +1 -1
- package/dist/tools/catalog-components.d.ts.map +1 -1
- package/dist/tools/catalog-components.js +1 -4
- package/dist/tools/catalog-components.js.map +1 -1
- package/dist/tools/create-ui.d.ts +3 -0
- package/dist/tools/create-ui.d.ts.map +1 -1
- package/dist/tools/create-ui.js +203 -75
- package/dist/tools/create-ui.js.map +1 -1
- package/dist/tools/detect-ui-stack.js.map +1 -1
- package/dist/tools/generate-component-variants.d.ts.map +1 -1
- package/dist/tools/generate-component-variants.js +15 -4
- package/dist/tools/generate-component-variants.js.map +1 -1
- package/dist/tools/generate-vibes.d.ts.map +1 -1
- package/dist/tools/generate-vibes.js +7 -3
- package/dist/tools/generate-vibes.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/modify-ui.d.ts.map +1 -1
- package/dist/tools/modify-ui.js +7 -2
- package/dist/tools/modify-ui.js.map +1 -1
- package/dist/tools/scaffold-project.d.ts.map +1 -1
- package/dist/tools/scaffold-project.js +3 -1
- package/dist/tools/scaffold-project.js.map +1 -1
- package/dist/tools/snippet-ui.d.ts +3 -1
- package/dist/tools/snippet-ui.d.ts.map +1 -1
- package/dist/tools/snippet-ui.js +219 -88
- package/dist/tools/snippet-ui.js.map +1 -1
- package/dist/tools/sync-design-tokens.d.ts.map +1 -1
- package/dist/tools/sync-design-tokens.js +26 -11
- package/dist/tools/sync-design-tokens.js.map +1 -1
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -55
- package/src/__tests__/builder.test.ts +19 -19
- package/src/__tests__/config.test.ts +63 -31
- package/src/__tests__/filter.test.ts +98 -92
- package/src/__tests__/remote-client.test.ts +179 -0
- package/src/components/catalog.ts +170 -166
- package/src/config/index.ts +185 -177
- package/src/context/builder.ts +157 -157
- package/src/context/filter.ts +110 -104
- package/src/context/grounding.ts +143 -129
- package/src/context/guards.ts +97 -38
- package/src/context/repo-hints.ts +24 -24
- package/src/context/styling-detector.ts +460 -0
- package/src/design/principles.ts +14 -14
- package/src/generation/gemini-client.ts +53 -56
- package/src/generation/litellm-client.ts +102 -86
- package/src/generation/remote-client.ts +100 -77
- package/src/index.ts +16 -16
- package/src/output/file-writer.ts +123 -123
- package/src/output/formatter.ts +139 -132
- package/src/server.ts +12 -11
- package/src/stack/detect.ts +226 -175
- package/src/tokens/sync.ts +189 -155
- package/src/tools/analyze-screenshot-ui.ts +89 -88
- package/src/tools/analyze-tokens.ts +80 -78
- package/src/tools/catalog-components.ts +68 -68
- package/src/tools/create-ui.ts +295 -142
- package/src/tools/detect-ui-stack.ts +36 -36
- package/src/tools/generate-component-variants.ts +155 -135
- package/src/tools/generate-vibes.ts +121 -117
- package/src/tools/index.ts +14 -14
- package/src/tools/modify-ui.ts +170 -165
- package/src/tools/scaffold-project.ts +68 -66
- package/src/tools/snippet-ui.ts +323 -172
- package/src/tools/sync-design-tokens.ts +217 -195
- package/src/utils/walk.ts +47 -45
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -33
- package/vitest.config.ts +10 -10
- package/.prettierrc +0 -9
- package/eslint.config.js +0 -37
package/src/tools/snippet-ui.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
- Accessible by default (WCAG AA): semantic HTML, labels, aria
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
260
|
+
if (propsInterface) {
|
|
261
|
+
userPrompt += `\n\nProps interface:\n${propsInterface}`;
|
|
262
|
+
}
|
|
155
263
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
264
|
+
if (animated) {
|
|
265
|
+
userPrompt += `\n\nInclude smooth, subtle animations/transitions (respect reduced motion).`;
|
|
266
|
+
}
|
|
159
267
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
}
|