@tpitre/story-ui 4.2.0 → 4.4.0

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/README.md CHANGED
@@ -10,7 +10,7 @@ Story UI revolutionizes component documentation by automatically generating Stor
10
10
  ## Why Story UI?
11
11
 
12
12
  - **Framework Agnostic**: Works with React, Vue, Angular, Svelte, and Web Components
13
- - **Multi-Provider AI**: Choose between Claude (Anthropic), GPT-5 (OpenAI), or Gemini (Google)
13
+ - **Multi-Provider AI**: Choose between Claude, OpenAI, or Google Gemini - always using the latest models
14
14
  - **Design System Aware**: Learns your component library and generates appropriate code
15
15
  - **Production Ready**: Deploy as a standalone web app with full MCP integration
16
16
  - **Zero Lock-in**: Use any component library - Mantine, Vuetify, Angular Material, Shoelace, or your own
@@ -47,6 +47,12 @@ Story UI will guide you through:
47
47
  - **TypeScript Support**: Full type-aware story generation
48
48
  - **Vision Support**: Attach screenshots for visual component requests
49
49
 
50
+ ### Story Management
51
+ - **Edit Existing Stories**: Modify any generated story through conversation
52
+ - **Delete Stories**: Remove stories directly from the Story UI panel
53
+ - **Orphan Detection**: Find and clean up stories without associated chat history
54
+ - **Full MCP Integration**: Manage stories via Claude Desktop or any MCP-compatible client
55
+
50
56
  ### Multi-Framework Support
51
57
 
52
58
  | Framework | Design Systems | Status |
@@ -54,16 +60,16 @@ Story UI will guide you through:
54
60
  | React | Mantine, Chakra UI, Material UI, Custom | Fully Supported |
55
61
  | Vue | Vuetify, Custom | Fully Supported |
56
62
  | Angular | Angular Material, Custom | Fully Supported |
57
- | Svelte | Skeleton UI, Custom | Fully Supported |
63
+ | Svelte | Flowbite-Svelte, Custom | Fully Supported |
58
64
  | Web Components | Shoelace, Custom | Fully Supported |
59
65
 
60
66
  ### Multi-Provider LLM Support
61
67
 
62
68
  | Provider | Models | Best For |
63
69
  |----------|--------|----------|
64
- | **Claude** (Anthropic) | Opus 4.5, Sonnet 4.5, Haiku 4.5 | Complex reasoning, code quality |
65
- | **GPT** (OpenAI) | GPT-5.1, GPT-5.1 Thinking, GPT-4o, GPT-4o Mini | Versatility, speed |
66
- | **Gemini** (Google) | Gemini 3 Pro, Gemini 2.0 Flash, Gemini 1.5 Pro | Fast generation, cost efficiency |
70
+ | **Claude** (Anthropic) | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 | Complex reasoning, code quality |
71
+ | **GPT** (OpenAI) | gpt-5.2, gpt-5.1, gpt-4o, gpt-4o-mini | Versatility, latest capabilities |
72
+ | **Gemini** (Google) | gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash | Fast generation, cost efficiency |
67
73
 
68
74
  ### Production Deployment
69
75
  - **Railway**: Node.js backend with file-based story persistence
@@ -115,7 +121,7 @@ The interactive installer will ask:
115
121
  ```
116
122
  ? Which AI provider do you prefer?
117
123
  > Claude (Anthropic) - Recommended
118
- OpenAI (GPT-5)
124
+ OpenAI
119
125
  Google Gemini
120
126
 
121
127
  ? Enter your API key:
@@ -256,11 +262,18 @@ The easiest way to connect is via Claude Desktop's built-in connector UI:
256
262
  2. Go to **Settings** → **Connectors**
257
263
  3. Click **"Add custom connector"**
258
264
  4. Enter:
259
- - **Name**: `Story UI` (or any name you prefer)
260
- - **URL**: `https://story-ui-demo.up.railway.app/mcp-remote/mcp` (production)
265
+ - **Name**: `Story UI React` (or any descriptive name)
266
+ - **URL**: Your deployed Railway URL + `/mcp-remote/mcp`
267
+ - Example: `https://your-app-name.up.railway.app/mcp-remote/mcp`
261
268
  5. Click **Add**
262
269
  6. **Restart Claude Desktop**
263
270
 
271
+ > **Note**: The URL will be your own Railway deployment URL. See [Production Deployment](#production-deployment) to set up your instance.
272
+
273
+ **Multiple Projects**: If you have multiple Storybook projects, add a separate connector for each:
274
+ - `Story UI React` → `https://my-react-app.up.railway.app/mcp-remote/mcp`
275
+ - `Story UI Vue` → `https://my-vue-app.up.railway.app/mcp-remote/mcp`
276
+
264
277
  Once connected, you'll have access to all Story UI tools directly in your Claude conversations:
265
278
  - `generate-story` - Generate Storybook stories from natural language
266
279
  - `list-components` - Discover available components
@@ -274,38 +287,52 @@ Once connected, you'll have access to all Story UI tools directly in your Claude
274
287
  Connect via Claude Code's built-in MCP support:
275
288
 
276
289
  ```bash
277
- # Add remote HTTP MCP server (production)
278
- claude mcp add --transport http story-ui https://story-ui-demo.up.railway.app/mcp-remote/mcp
290
+ # Add your production Railway deployment
291
+ claude mcp add --transport http story-ui-react https://your-react-app.up.railway.app/mcp-remote/mcp
279
292
 
280
- # Or for local development
281
- claude mcp add --transport http story-ui-local http://localhost:4005/mcp-remote/mcp
293
+ # Add another project (if needed)
294
+ claude mcp add --transport http story-ui-vue https://your-vue-app.up.railway.app/mcp-remote/mcp
295
+
296
+ # For local development (default port is 4001)
297
+ claude mcp add --transport http story-ui-local http://localhost:4001/mcp-remote/mcp
282
298
  ```
283
299
 
284
300
  ### Manual Configuration (Advanced)
285
301
 
286
- For advanced users who prefer manual configuration, add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
302
+ For running multiple local Story UI instances with different ports, configure your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
287
303
 
288
304
  ```json
289
305
  {
290
306
  "mcpServers": {
291
- "story-ui": {
307
+ "story-ui-react": {
308
+ "command": "npx",
309
+ "args": ["@tpitre/story-ui", "start", "--port", "4001"]
310
+ },
311
+ "story-ui-vue": {
312
+ "command": "npx",
313
+ "args": ["@tpitre/story-ui", "start", "--port", "4002"]
314
+ },
315
+ "story-ui-angular": {
292
316
  "command": "npx",
293
- "args": ["@tpitre/story-ui", "mcp"],
294
- "env": {
295
- "ANTHROPIC_API_KEY": "your-api-key"
296
- }
317
+ "args": ["@tpitre/story-ui", "start", "--port", "4003"]
297
318
  }
298
319
  }
299
320
  }
300
321
  ```
301
322
 
323
+ > **Note**: When using Claude Desktop, API keys are managed through your Anthropic account - no need to configure them in the MCP server.
324
+
302
325
  ### Starting the Local MCP Server
303
326
 
304
327
  ```bash
328
+ # Start with default port (4001)
305
329
  npx story-ui start
330
+
331
+ # Or specify a custom port
332
+ npx story-ui start --port 4002
306
333
  ```
307
334
 
308
- This starts the Story UI HTTP server with MCP endpoint at `http://localhost:4005/mcp-remote/mcp`.
335
+ This starts the Story UI HTTP server with MCP endpoint at `http://localhost:<port>/mcp-remote/mcp`.
309
336
 
310
337
  ### Available MCP Commands
311
338
 
@@ -319,13 +346,13 @@ Once connected, you can use these commands in Claude Desktop:
319
346
 
320
347
  ## Production Deployment
321
348
 
322
- Story UI v3 can be deployed as a standalone web application accessible from anywhere.
349
+ Story UI can be deployed as a standalone web application accessible from anywhere. We recommend Railway for its ease of use, but any Node.js hosting platform will work.
323
350
 
324
351
  ### Architecture
325
352
 
326
353
  ```
327
354
  ┌─────────────────────────────────────────────────────────────┐
328
- Railway Deployment
355
+ Your Deployment (e.g., Railway)
329
356
  │ ┌─────────────────────────────────────────────────────────┐│
330
357
  │ │ Express MCP Server (Node.js) ││
331
358
  │ │ - Serves Storybook with Story UI addon ││
@@ -336,9 +363,9 @@ Story UI v3 can be deployed as a standalone web application accessible from anyw
336
363
  └──────────────────────────────────────────────────────────────┘
337
364
  ```
338
365
 
339
- ### Deploy to Railway
366
+ ### Deploy to Railway (Recommended)
340
367
 
341
- Railway provides a straightforward deployment experience with file-based story persistence.
368
+ Railway provides a straightforward deployment experience with automatic HTTPS and file-based story persistence.
342
369
 
343
370
  **Quick Start:**
344
371
 
@@ -347,23 +374,27 @@ Railway provides a straightforward deployment experience with file-based story p
347
374
  npm install -g @railway/cli
348
375
  railway login
349
376
 
350
- # Initialize and deploy
377
+ # Initialize and deploy from your Storybook project
351
378
  railway init
352
379
  railway up
353
380
  ```
354
381
 
355
382
  **Environment Variables (set in Railway Dashboard):**
356
383
  - `ANTHROPIC_API_KEY` - Required for Claude models
357
- - `OPENAI_API_KEY` - Optional for OpenAI models
358
- - `GEMINI_API_KEY` - Optional for Gemini models
384
+ - `OPENAI_API_KEY` - Optional, for OpenAI models
385
+ - `GEMINI_API_KEY` - Optional, for Gemini models
386
+
387
+ **After Deployment:**
359
388
 
360
- **Connect MCP Clients:**
389
+ Your Railway app will have a URL like `https://your-app-name.up.railway.app`. Use this URL to connect MCP clients:
361
390
 
362
391
  ```bash
363
- # Add your Railway deployment as an MCP server
364
- claude mcp add --transport http story-ui https://your-app.up.railway.app/mcp-remote/mcp
392
+ # In Claude Code
393
+ claude mcp add --transport http story-ui https://your-app-name.up.railway.app/mcp-remote/mcp
365
394
  ```
366
395
 
396
+ Or add it to Claude Desktop via **Settings** → **Connectors** → **Add custom connector**.
397
+
367
398
  See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions and troubleshooting.
368
399
 
369
400
  ---
@@ -417,17 +448,14 @@ For simpler setups, use `story-ui-considerations.md`:
417
448
  ## CLI Reference
418
449
 
419
450
  ```bash
420
- # Initialize Story UI
451
+ # Initialize Story UI in your project
421
452
  npx story-ui init
422
453
 
423
- # Start the development server
454
+ # Start the MCP server (default port: 4001)
424
455
  npx story-ui start
425
- npx story-ui start --port 4005
426
-
427
- # Deploy to production
428
- npx story-ui deploy
456
+ npx story-ui start --port 4002 # Custom port
429
457
 
430
- # Run MCP server
458
+ # Run MCP STDIO server (for Claude Desktop local integration)
431
459
  npx story-ui mcp
432
460
  ```
433
461
 
@@ -461,7 +489,7 @@ npx story-ui mcp
461
489
 
462
490
  ## Upgrading from v2
463
491
 
464
- Story UI v3 is backwards compatible with v2 configurations. However, to take advantage of new features:
492
+ Story UI v4 is backwards compatible with previous configurations. However, to take advantage of new features:
465
493
 
466
494
  1. **Multi-Provider Support**: Add `llmProvider` to your config
467
495
  2. **Framework Detection**: Add `framework` to your config for non-React projects
@@ -78,6 +78,176 @@ app.get('/mcp/frameworks/detect', detectCurrentFramework);
78
78
  app.get('/mcp/frameworks/:type', getFrameworkDetails);
79
79
  app.post('/mcp/frameworks/validate', validateStoryForFramework);
80
80
  app.post('/mcp/frameworks/post-process', postProcessStoryForFramework);
81
+ // MCP story management routes - for Claude Desktop and other MCP clients
82
+ // List all stories
83
+ app.get('/mcp/stories', async (req, res) => {
84
+ try {
85
+ const storiesPath = config.generatedStoriesPath;
86
+ if (!fs.existsSync(storiesPath)) {
87
+ return res.json({ stories: [] });
88
+ }
89
+ const files = fs.readdirSync(storiesPath);
90
+ const stories = files
91
+ .filter(file => file.endsWith('.stories.tsx') || file.endsWith('.stories.ts') || file.endsWith('.stories.svelte'))
92
+ .map(file => {
93
+ const filePath = path.join(storiesPath, file);
94
+ const stats = fs.statSync(filePath);
95
+ const content = fs.readFileSync(filePath, 'utf-8');
96
+ // Extract title from story file
97
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
98
+ let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace(/\.stories\.(tsx|ts|svelte)$/, '');
99
+ // Remove hash suffix from display title
100
+ title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
101
+ return {
102
+ id: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
103
+ storyId: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
104
+ fileName: file,
105
+ title,
106
+ lastUpdated: stats.mtime.getTime(),
107
+ createdAt: stats.birthtime.getTime(),
108
+ content
109
+ };
110
+ })
111
+ .sort((a, b) => b.lastUpdated - a.lastUpdated);
112
+ return res.json({ stories });
113
+ }
114
+ catch (error) {
115
+ console.error('Error listing stories:', error);
116
+ return res.status(500).json({ error: 'Failed to list stories' });
117
+ }
118
+ });
119
+ // Get a specific story by ID
120
+ app.get('/mcp/stories/:storyId', async (req, res) => {
121
+ try {
122
+ const { storyId } = req.params;
123
+ const storiesPath = config.generatedStoriesPath;
124
+ if (!fs.existsSync(storiesPath)) {
125
+ return res.status(404).json({ error: 'Stories directory not found' });
126
+ }
127
+ const files = fs.readdirSync(storiesPath);
128
+ // Extract hash from story ID if in legacy format (story-a1b2c3d4)
129
+ const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
130
+ const hash = hashMatch ? hashMatch[1] : null;
131
+ // Find matching file
132
+ const matchingFile = files.find(file => {
133
+ // Match by hash suffix
134
+ if (hash && file.includes(`-${hash}.stories.`))
135
+ return true;
136
+ // Match by exact ID
137
+ if (file.startsWith(`${storyId}.stories.`))
138
+ return true;
139
+ // Match by fileName
140
+ if (file === storyId)
141
+ return true;
142
+ // Match by ID without extension
143
+ if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
144
+ return true;
145
+ return false;
146
+ });
147
+ if (!matchingFile) {
148
+ return res.status(404).json({ error: `Story with ID ${storyId} not found` });
149
+ }
150
+ const filePath = path.join(storiesPath, matchingFile);
151
+ const stats = fs.statSync(filePath);
152
+ const content = fs.readFileSync(filePath, 'utf-8');
153
+ // Extract title from story file
154
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
155
+ let title = titleMatch ? titleMatch[1].replace('Generated/', '') : matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, '');
156
+ title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
157
+ return res.json({
158
+ id: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
159
+ storyId: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
160
+ fileName: matchingFile,
161
+ title,
162
+ lastUpdated: stats.mtime.getTime(),
163
+ createdAt: stats.birthtime.getTime(),
164
+ timestamp: stats.mtime.getTime(),
165
+ content,
166
+ story: content
167
+ });
168
+ }
169
+ catch (error) {
170
+ console.error('Error getting story:', error);
171
+ return res.status(500).json({ error: 'Failed to get story' });
172
+ }
173
+ });
174
+ // Get story content (raw code)
175
+ app.get('/mcp/stories/:storyId/content', async (req, res) => {
176
+ try {
177
+ const { storyId } = req.params;
178
+ const storiesPath = config.generatedStoriesPath;
179
+ if (!fs.existsSync(storiesPath)) {
180
+ return res.status(404).send('Stories directory not found');
181
+ }
182
+ const files = fs.readdirSync(storiesPath);
183
+ // Extract hash from story ID if in legacy format
184
+ const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
185
+ const hash = hashMatch ? hashMatch[1] : null;
186
+ // Find matching file
187
+ const matchingFile = files.find(file => {
188
+ if (hash && file.includes(`-${hash}.stories.`))
189
+ return true;
190
+ if (file.startsWith(`${storyId}.stories.`))
191
+ return true;
192
+ if (file === storyId)
193
+ return true;
194
+ if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
195
+ return true;
196
+ return false;
197
+ });
198
+ if (!matchingFile) {
199
+ return res.status(404).send(`Story with ID ${storyId} not found`);
200
+ }
201
+ const filePath = path.join(storiesPath, matchingFile);
202
+ const content = fs.readFileSync(filePath, 'utf-8');
203
+ res.type('text/plain').send(content);
204
+ }
205
+ catch (error) {
206
+ console.error('Error getting story content:', error);
207
+ return res.status(500).send('Failed to get story content');
208
+ }
209
+ });
210
+ // Delete a story by ID
211
+ app.delete('/mcp/stories/:storyId', async (req, res) => {
212
+ try {
213
+ const { storyId } = req.params;
214
+ const storiesPath = config.generatedStoriesPath;
215
+ if (!fs.existsSync(storiesPath)) {
216
+ return res.status(404).json({ error: 'Stories directory not found' });
217
+ }
218
+ const files = fs.readdirSync(storiesPath);
219
+ // Extract hash from story ID if in legacy format
220
+ const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
221
+ const hash = hashMatch ? hashMatch[1] : null;
222
+ // Find matching file
223
+ const matchingFile = files.find(file => {
224
+ if (hash && file.includes(`-${hash}.stories.`))
225
+ return true;
226
+ if (file.startsWith(`${storyId}.stories.`))
227
+ return true;
228
+ if (file === storyId)
229
+ return true;
230
+ if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
231
+ return true;
232
+ return false;
233
+ });
234
+ if (!matchingFile) {
235
+ return res.status(404).json({ error: `Story with ID ${storyId} not found` });
236
+ }
237
+ const filePath = path.join(storiesPath, matchingFile);
238
+ fs.unlinkSync(filePath);
239
+ console.log(`🗑️ Deleted story via MCP endpoint: ${matchingFile}`);
240
+ return res.json({
241
+ success: true,
242
+ deleted: matchingFile,
243
+ message: `Story "${matchingFile}" has been deleted successfully.`
244
+ });
245
+ }
246
+ catch (error) {
247
+ console.error('Error deleting story:', error);
248
+ return res.status(500).json({ error: 'Failed to delete story' });
249
+ }
250
+ });
81
251
  // File-based story routes - stories are generated as .stories.tsx files
82
252
  // Storybook discovers these automatically via its native file system watching
83
253
  // Proxy routes for frontend compatibility (maps /story-ui/ to /mcp/)
@@ -27,7 +27,8 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
27
27
  process.env.STORY_UI_MCP_MODE = 'true';
28
28
  // Get HTTP server port from environment variables (check multiple possible names)
29
29
  const HTTP_PORT = process.env.VITE_STORY_UI_PORT || process.env.STORY_UI_HTTP_PORT || process.env.PORT || '4001';
30
- const HTTP_BASE_URL = `http://localhost:${HTTP_PORT}`;
30
+ // Allow configurable base URL for Railway/cloud deployments, fallback to localhost for local dev
31
+ const HTTP_BASE_URL = process.env.STORY_UI_HTTP_BASE_URL || `http://localhost:${HTTP_PORT}`;
31
32
  // Initialize configuration
32
33
  const config = loadUserConfig();
33
34
  // Create MCP server instance
@@ -181,8 +182,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
181
182
  throw new Error(`Failed to generate story: ${error}`);
182
183
  }
183
184
  const result = await response.json();
184
- // Debug log to see what we're getting
185
- console.error('Story generation result:', JSON.stringify(result, null, 2));
186
185
  return {
187
186
  content: [{
188
187
  type: "text",
@@ -509,8 +508,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
509
508
  throw new Error(`Failed to update story: ${error}`);
510
509
  }
511
510
  const result = await response.json();
512
- // Debug log to see what we're getting
513
- console.error('Story update result:', JSON.stringify(result, null, 2));
514
511
  return {
515
512
  content: [{
516
513
  type: "text",
@@ -150,7 +150,7 @@ class AdapterRegistry {
150
150
  return {
151
151
  systemPrompt: adapter.generateSystemPrompt(config, options),
152
152
  componentReference: adapter.generateComponentReference(components, config),
153
- layoutInstructions: '', // TODO: Implement layout instructions
153
+ layoutInstructions: '',
154
154
  examples: adapter.generateExamples(config),
155
155
  sampleStory: adapter.generateSampleStory(config, components),
156
156
  framework: this.detectedFramework || {
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAwnCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CA0sCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -452,12 +452,79 @@ function StoryUIPanel({ mcpPort }) {
452
452
  const fileInputRef = useRef(null);
453
453
  const abortControllerRef = useRef(null);
454
454
  const hasShownRefreshHint = useRef(false);
455
+ // Track stories for MCP external generation detection
456
+ // Used to detect when stories are created via MCP remote (Claude Desktop)
457
+ // and trigger automatic refresh since MCP has no browser context
458
+ const panelGeneratedStoryIds = useRef(new Set());
459
+ const knownStoryIds = useRef(new Set());
460
+ const isPollingInitialized = useRef(false);
455
461
  // Set port override if provided
456
462
  useEffect(() => {
457
463
  if (mcpPort && typeof window !== 'undefined') {
458
464
  window.STORY_UI_MCP_PORT = String(mcpPort);
459
465
  }
460
466
  }, [mcpPort]);
467
+ // Poll for MCP-generated stories (stories created externally via Claude Desktop/Code)
468
+ // This solves the Vite HMR issue where stories generated via MCP remote don't trigger
469
+ // a browser refresh because MCP has no browser context to call window.location.reload()
470
+ useEffect(() => {
471
+ const POLL_INTERVAL_MS = 5000; // Check every 5 seconds
472
+ const pollForExternalStories = async () => {
473
+ try {
474
+ const baseUrl = getApiBaseUrl();
475
+ const response = await fetch(`${baseUrl}/story-ui/stories`);
476
+ if (!response.ok)
477
+ return;
478
+ const data = await response.json();
479
+ const currentStoryIds = new Set(data.stories?.map((s) => s.id) || []);
480
+ // On first poll, just record what's already there
481
+ if (!isPollingInitialized.current) {
482
+ knownStoryIds.current = currentStoryIds;
483
+ isPollingInitialized.current = true;
484
+ console.log('[Story UI] MCP story polling initialized with', currentStoryIds.size, 'stories');
485
+ return;
486
+ }
487
+ // Check for new stories not created by this panel session
488
+ for (const storyId of currentStoryIds) {
489
+ if (!knownStoryIds.current.has(storyId) && !panelGeneratedStoryIds.current.has(storyId)) {
490
+ // New story detected that wasn't created by this panel - must be from MCP remote
491
+ console.log('[Story UI] Detected externally generated story:', storyId);
492
+ console.log('[Story UI] Triggering refresh to register new story in Vite import map...');
493
+ // Update known stories before refresh
494
+ knownStoryIds.current = currentStoryIds;
495
+ // Trigger refresh with a short delay
496
+ setTimeout(() => {
497
+ try {
498
+ if (window.top && window.top !== window) {
499
+ window.top.location.reload();
500
+ }
501
+ else if (window.parent && window.parent !== window) {
502
+ window.parent.location.reload();
503
+ }
504
+ else {
505
+ window.location.reload();
506
+ }
507
+ }
508
+ catch (error) {
509
+ console.warn('[Story UI] Could not auto-refresh for MCP-generated story');
510
+ }
511
+ }, 1000);
512
+ return; // Only trigger one refresh
513
+ }
514
+ }
515
+ // Update known stories
516
+ knownStoryIds.current = currentStoryIds;
517
+ }
518
+ catch (error) {
519
+ // Silently ignore polling errors - server may be unavailable temporarily
520
+ }
521
+ };
522
+ // Start polling
523
+ const intervalId = setInterval(pollForExternalStories, POLL_INTERVAL_MS);
524
+ // Initial poll
525
+ pollForExternalStories();
526
+ return () => clearInterval(intervalId);
527
+ }, []);
461
528
  // Detect Storybook theme
462
529
  useEffect(() => {
463
530
  const detectTheme = () => {
@@ -863,6 +930,13 @@ function StoryUIPanel({ mcpPort }) {
863
930
  title: completion.title,
864
931
  action: completion.summary?.action,
865
932
  });
933
+ // Track this story as panel-generated to prevent false MCP detection
934
+ // The story ID is the fileName without .stories.tsx extension
935
+ if (completion.success && completion.fileName) {
936
+ const storyId = completion.fileName.replace('.stories.tsx', '');
937
+ panelGeneratedStoryIds.current.add(storyId);
938
+ console.log('[Story UI] Tracking panel-generated story:', storyId);
939
+ }
866
940
  const isUpdate = completion.summary.action === 'updated';
867
941
  const responseMessage = buildConversationalResponse(completion, isUpdate);
868
942
  const aiMsg = { role: 'ai', content: responseMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -800,6 +800,13 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
800
800
  const abortControllerRef = useRef<AbortController | null>(null);
801
801
  const hasShownRefreshHint = useRef(false);
802
802
 
803
+ // Track stories for MCP external generation detection
804
+ // Used to detect when stories are created via MCP remote (Claude Desktop)
805
+ // and trigger automatic refresh since MCP has no browser context
806
+ const panelGeneratedStoryIds = useRef<Set<string>>(new Set());
807
+ const knownStoryIds = useRef<Set<string>>(new Set());
808
+ const isPollingInitialized = useRef(false);
809
+
803
810
  // Set port override if provided
804
811
  useEffect(() => {
805
812
  if (mcpPort && typeof window !== 'undefined') {
@@ -807,6 +814,73 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
807
814
  }
808
815
  }, [mcpPort]);
809
816
 
817
+ // Poll for MCP-generated stories (stories created externally via Claude Desktop/Code)
818
+ // This solves the Vite HMR issue where stories generated via MCP remote don't trigger
819
+ // a browser refresh because MCP has no browser context to call window.location.reload()
820
+ useEffect(() => {
821
+ const POLL_INTERVAL_MS = 5000; // Check every 5 seconds
822
+
823
+ const pollForExternalStories = async () => {
824
+ try {
825
+ const baseUrl = getApiBaseUrl();
826
+ const response = await fetch(`${baseUrl}/story-ui/stories`);
827
+ if (!response.ok) return;
828
+
829
+ const data = await response.json();
830
+ const currentStoryIds = new Set<string>(data.stories?.map((s: { id: string }) => s.id) || []);
831
+
832
+ // On first poll, just record what's already there
833
+ if (!isPollingInitialized.current) {
834
+ knownStoryIds.current = currentStoryIds;
835
+ isPollingInitialized.current = true;
836
+ console.log('[Story UI] MCP story polling initialized with', currentStoryIds.size, 'stories');
837
+ return;
838
+ }
839
+
840
+ // Check for new stories not created by this panel session
841
+ for (const storyId of currentStoryIds) {
842
+ if (!knownStoryIds.current.has(storyId) && !panelGeneratedStoryIds.current.has(storyId)) {
843
+ // New story detected that wasn't created by this panel - must be from MCP remote
844
+ console.log('[Story UI] Detected externally generated story:', storyId);
845
+ console.log('[Story UI] Triggering refresh to register new story in Vite import map...');
846
+
847
+ // Update known stories before refresh
848
+ knownStoryIds.current = currentStoryIds;
849
+
850
+ // Trigger refresh with a short delay
851
+ setTimeout(() => {
852
+ try {
853
+ if (window.top && window.top !== window) {
854
+ window.top.location.reload();
855
+ } else if (window.parent && window.parent !== window) {
856
+ window.parent.location.reload();
857
+ } else {
858
+ window.location.reload();
859
+ }
860
+ } catch (error) {
861
+ console.warn('[Story UI] Could not auto-refresh for MCP-generated story');
862
+ }
863
+ }, 1000);
864
+ return; // Only trigger one refresh
865
+ }
866
+ }
867
+
868
+ // Update known stories
869
+ knownStoryIds.current = currentStoryIds;
870
+ } catch (error) {
871
+ // Silently ignore polling errors - server may be unavailable temporarily
872
+ }
873
+ };
874
+
875
+ // Start polling
876
+ const intervalId = setInterval(pollForExternalStories, POLL_INTERVAL_MS);
877
+
878
+ // Initial poll
879
+ pollForExternalStories();
880
+
881
+ return () => clearInterval(intervalId);
882
+ }, []);
883
+
810
884
  // Detect Storybook theme
811
885
  useEffect(() => {
812
886
  const detectTheme = () => {
@@ -1219,6 +1293,14 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1219
1293
  action: completion.summary?.action,
1220
1294
  });
1221
1295
 
1296
+ // Track this story as panel-generated to prevent false MCP detection
1297
+ // The story ID is the fileName without .stories.tsx extension
1298
+ if (completion.success && completion.fileName) {
1299
+ const storyId = completion.fileName.replace('.stories.tsx', '');
1300
+ panelGeneratedStoryIds.current.add(storyId);
1301
+ console.log('[Story UI] Tracking panel-generated story:', storyId);
1302
+ }
1303
+
1222
1304
  const isUpdate = completion.summary.action === 'updated';
1223
1305
  const responseMessage = buildConversationalResponse(completion, isUpdate);
1224
1306
  const aiMsg: Message = { role: 'ai', content: responseMessage };