@tpitre/story-ui 4.1.2 → 4.2.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/dist/cli/index.js CHANGED
File without changes
@@ -399,6 +399,125 @@ app.delete('/story-ui/stories', async (req, res) => {
399
399
  return res.status(500).json({ error: 'Failed to clear stories' });
400
400
  }
401
401
  });
402
+ // Orphan stories management - find and delete stories without associated chats
403
+ // POST to get list of orphans, DELETE to remove them
404
+ app.post('/story-ui/orphan-stories', async (req, res) => {
405
+ try {
406
+ const { chatFileNames } = req.body;
407
+ if (!chatFileNames || !Array.isArray(chatFileNames)) {
408
+ return res.status(400).json({ error: 'chatFileNames array is required' });
409
+ }
410
+ const storiesPath = config.generatedStoriesPath;
411
+ if (!fs.existsSync(storiesPath)) {
412
+ return res.json({ orphans: [], count: 0 });
413
+ }
414
+ const files = fs.readdirSync(storiesPath);
415
+ const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
416
+ file.endsWith('.stories.ts') ||
417
+ file.endsWith('.stories.svelte'));
418
+ // Find orphans: stories that don't match any chat fileName
419
+ const orphans = storyFiles.filter(storyFile => {
420
+ // Extract the base name without extension for comparison
421
+ const storyBase = storyFile
422
+ .replace('.stories.tsx', '')
423
+ .replace('.stories.ts', '')
424
+ .replace('.stories.svelte', '');
425
+ // Check if any chat fileName matches this story
426
+ return !chatFileNames.some(chatFileName => {
427
+ if (!chatFileName)
428
+ return false;
429
+ const chatBase = chatFileName
430
+ .replace('.stories.tsx', '')
431
+ .replace('.stories.ts', '')
432
+ .replace('.stories.svelte', '');
433
+ return storyBase === chatBase || storyFile === chatFileName;
434
+ });
435
+ });
436
+ // Get details for each orphan
437
+ const orphanDetails = orphans.map(fileName => {
438
+ const filePath = path.join(storiesPath, fileName);
439
+ const content = fs.readFileSync(filePath, 'utf-8');
440
+ const stats = fs.statSync(filePath);
441
+ // Extract title from story file
442
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
443
+ let title = titleMatch ? titleMatch[1].replace('Generated/', '') : fileName.replace(/\.stories\.[a-z]+$/, '');
444
+ // Remove hash suffix from display title
445
+ title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
446
+ return {
447
+ fileName,
448
+ title,
449
+ lastUpdated: stats.mtime.getTime()
450
+ };
451
+ });
452
+ console.log(`📋 Found ${orphans.length} orphan stories out of ${storyFiles.length} total`);
453
+ return res.json({
454
+ orphans: orphanDetails,
455
+ count: orphans.length,
456
+ totalStories: storyFiles.length
457
+ });
458
+ }
459
+ catch (error) {
460
+ console.error('Error finding orphan stories:', error);
461
+ return res.status(500).json({ error: 'Failed to find orphan stories' });
462
+ }
463
+ });
464
+ app.delete('/story-ui/orphan-stories', async (req, res) => {
465
+ try {
466
+ const { chatFileNames } = req.body;
467
+ if (!chatFileNames || !Array.isArray(chatFileNames)) {
468
+ return res.status(400).json({ error: 'chatFileNames array is required' });
469
+ }
470
+ const storiesPath = config.generatedStoriesPath;
471
+ if (!fs.existsSync(storiesPath)) {
472
+ return res.json({ deleted: [], count: 0 });
473
+ }
474
+ const files = fs.readdirSync(storiesPath);
475
+ const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
476
+ file.endsWith('.stories.ts') ||
477
+ file.endsWith('.stories.svelte'));
478
+ // Find orphans: stories that don't match any chat fileName
479
+ const orphans = storyFiles.filter(storyFile => {
480
+ const storyBase = storyFile
481
+ .replace('.stories.tsx', '')
482
+ .replace('.stories.ts', '')
483
+ .replace('.stories.svelte', '');
484
+ return !chatFileNames.some(chatFileName => {
485
+ if (!chatFileName)
486
+ return false;
487
+ const chatBase = chatFileName
488
+ .replace('.stories.tsx', '')
489
+ .replace('.stories.ts', '')
490
+ .replace('.stories.svelte', '');
491
+ return storyBase === chatBase || storyFile === chatFileName;
492
+ });
493
+ });
494
+ // Delete orphans
495
+ const deleted = [];
496
+ const errors = [];
497
+ for (const fileName of orphans) {
498
+ try {
499
+ const filePath = path.join(storiesPath, fileName);
500
+ fs.unlinkSync(filePath);
501
+ deleted.push(fileName);
502
+ console.log(`🗑️ Deleted orphan story: ${fileName}`);
503
+ }
504
+ catch (err) {
505
+ errors.push(fileName);
506
+ console.error(`❌ Error deleting orphan ${fileName}:`, err);
507
+ }
508
+ }
509
+ console.log(`✅ Deleted ${deleted.length} orphan stories`);
510
+ return res.json({
511
+ deleted,
512
+ count: deleted.length,
513
+ errors: errors.length > 0 ? errors : undefined
514
+ });
515
+ }
516
+ catch (error) {
517
+ console.error('Error deleting orphan stories:', error);
518
+ return res.status(500).json({ error: 'Failed to delete orphan stories' });
519
+ }
520
+ });
402
521
  // MCP Remote HTTP transport routes (for Claude Desktop remote connections)
403
522
  // Provides Streamable HTTP and legacy SSE endpoints for remote MCP access
404
523
  app.use('/mcp-remote', mcpRemoteRouter);
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAuwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAsiCnD;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,2CAwnCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -151,6 +151,7 @@ const MCP_API = `${API_BASE}/mcp/generate-story`;
151
151
  const MCP_STREAM_API = `${API_BASE}/mcp/generate-story-stream`;
152
152
  const PROVIDERS_API = `${API_BASE}/mcp/providers`;
153
153
  const STORIES_API = `${API_BASE}/story-ui/stories`;
154
+ const ORPHAN_STORIES_API = `${API_BASE}/story-ui/orphan-stories`;
154
155
  const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
155
156
  function isEdgeMode() {
156
157
  if (typeof window !== 'undefined') {
@@ -444,6 +445,8 @@ function StoryUIPanel({ mcpPort }) {
444
445
  const [contextMenuId, setContextMenuId] = useState(null);
445
446
  const [renamingChatId, setRenamingChatId] = useState(null);
446
447
  const [renameValue, setRenameValue] = useState('');
448
+ const [orphanCount, setOrphanCount] = useState(0);
449
+ const [isDeletingOrphans, setIsDeletingOrphans] = useState(false);
447
450
  const chatEndRef = useRef(null);
448
451
  const inputRef = useRef(null);
449
452
  const fileInputRef = useRef(null);
@@ -1144,6 +1147,65 @@ function StoryUIPanel({ mcpPort }) {
1144
1147
  }
1145
1148
  }
1146
1149
  };
1150
+ // Check for orphan stories (stories without associated chats)
1151
+ const checkOrphanStories = useCallback(async () => {
1152
+ if (!state.connectionStatus.connected)
1153
+ return;
1154
+ try {
1155
+ const chatFileNames = state.recentChats.map(chat => chat.fileName);
1156
+ const response = await fetch(ORPHAN_STORIES_API, {
1157
+ method: 'POST',
1158
+ headers: { 'Content-Type': 'application/json' },
1159
+ body: JSON.stringify({ chatFileNames }),
1160
+ });
1161
+ if (response.ok) {
1162
+ const data = await response.json();
1163
+ setOrphanCount(data.count || 0);
1164
+ }
1165
+ }
1166
+ catch (error) {
1167
+ console.error('Failed to check orphan stories:', error);
1168
+ }
1169
+ }, [state.connectionStatus.connected, state.recentChats]);
1170
+ // Delete all orphan stories
1171
+ const handleDeleteOrphans = async () => {
1172
+ if (orphanCount === 0)
1173
+ return;
1174
+ if (!confirm(`Delete ${orphanCount} orphan ${orphanCount === 1 ? 'story' : 'stories'}? These are generated story files without associated chats.`)) {
1175
+ return;
1176
+ }
1177
+ setIsDeletingOrphans(true);
1178
+ try {
1179
+ const chatFileNames = state.recentChats.map(chat => chat.fileName);
1180
+ const response = await fetch(ORPHAN_STORIES_API, {
1181
+ method: 'DELETE',
1182
+ headers: { 'Content-Type': 'application/json' },
1183
+ body: JSON.stringify({ chatFileNames }),
1184
+ });
1185
+ if (response.ok) {
1186
+ const data = await response.json();
1187
+ setOrphanCount(0);
1188
+ if (data.count > 0) {
1189
+ // Show success message briefly
1190
+ alert(`Deleted ${data.count} orphan ${data.count === 1 ? 'story' : 'stories'}.`);
1191
+ }
1192
+ }
1193
+ else {
1194
+ alert('Failed to delete orphan stories. Please try again.');
1195
+ }
1196
+ }
1197
+ catch (error) {
1198
+ console.error('Failed to delete orphan stories:', error);
1199
+ alert('Failed to delete orphan stories. Please try again.');
1200
+ }
1201
+ finally {
1202
+ setIsDeletingOrphans(false);
1203
+ }
1204
+ };
1205
+ // Check for orphans when chats change or connection is established
1206
+ useEffect(() => {
1207
+ checkOrphanStories();
1208
+ }, [checkOrphanStories]);
1147
1209
  const handleStartRename = (chatId, currentTitle, e) => {
1148
1210
  if (e)
1149
1211
  e.stopPropagation();
@@ -1255,7 +1317,7 @@ function StoryUIPanel({ mcpPort }) {
1255
1317
  handleConfirmRename(chat.id);
1256
1318
  if (e.key === 'Escape')
1257
1319
  handleCancelRename();
1258
- }, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) })] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }), _jsx("div", { className: "sui-header-right", children: state.connectionStatus.connected && state.availableProviders.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: state.availableProviders.find(p => p.type === state.selectedProvider)?.name || 'Provider' }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedProvider, onChange: e => {
1320
+ }, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) }), orphanCount > 0 && (_jsx("div", { className: "sui-orphan-footer", children: _jsx("button", { className: "sui-orphan-delete-btn", onClick: handleDeleteOrphans, disabled: isDeletingOrphans, title: `${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`, children: isDeletingOrphans ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sui-orphan-spinner" }), _jsx("span", { children: "Deleting..." })] })) : (_jsxs(_Fragment, { children: [Icons.trash, _jsxs("span", { children: [orphanCount, " orphan ", orphanCount === 1 ? 'story' : 'stories'] })] })) }) }))] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }), _jsx("div", { className: "sui-header-right", children: state.connectionStatus.connected && state.availableProviders.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: state.availableProviders.find(p => p.type === state.selectedProvider)?.name || 'Provider' }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedProvider, onChange: e => {
1259
1321
  const newProvider = e.target.value;
1260
1322
  dispatch({ type: 'SET_SELECTED_PROVIDER', payload: newProvider });
1261
1323
  const provider = state.availableProviders.find(p => p.type === newProvider);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.1.2",
3
+ "version": "4.2.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1467,3 +1467,59 @@
1467
1467
  .sui-hidden {
1468
1468
  display: none;
1469
1469
  }
1470
+
1471
+ /* ============================================
1472
+ Orphan Stories Footer
1473
+ ============================================ */
1474
+ .sui-orphan-footer {
1475
+ flex-shrink: 0;
1476
+ padding-top: var(--space-2);
1477
+ border-top: 1px solid hsl(var(--border));
1478
+ margin-top: auto;
1479
+ }
1480
+
1481
+ .sui-orphan-delete-btn {
1482
+ display: flex;
1483
+ align-items: center;
1484
+ gap: var(--space-2);
1485
+ width: 100%;
1486
+ padding: var(--space-2) var(--space-3);
1487
+ border: none;
1488
+ background: transparent;
1489
+ color: hsl(var(--muted-foreground));
1490
+ font-size: 0.75rem;
1491
+ cursor: pointer;
1492
+ border-radius: var(--radius-sm);
1493
+ transition: all var(--transition-fast);
1494
+ }
1495
+
1496
+ .sui-orphan-delete-btn:hover:not(:disabled) {
1497
+ background: hsl(var(--destructive) / 0.1);
1498
+ color: hsl(var(--destructive));
1499
+ }
1500
+
1501
+ .sui-orphan-delete-btn:disabled {
1502
+ cursor: not-allowed;
1503
+ opacity: 0.6;
1504
+ }
1505
+
1506
+ .sui-orphan-delete-btn svg {
1507
+ width: 14px;
1508
+ height: 14px;
1509
+ flex-shrink: 0;
1510
+ }
1511
+
1512
+ .sui-orphan-spinner {
1513
+ width: 14px;
1514
+ height: 14px;
1515
+ border: 2px solid hsl(var(--muted-foreground) / 0.3);
1516
+ border-top-color: hsl(var(--muted-foreground));
1517
+ border-radius: 50%;
1518
+ animation: sui-spin 0.6s linear infinite;
1519
+ }
1520
+
1521
+ @keyframes sui-spin {
1522
+ to {
1523
+ transform: rotate(360deg);
1524
+ }
1525
+ }
@@ -336,6 +336,7 @@ const MCP_API = `${API_BASE}/mcp/generate-story`;
336
336
  const MCP_STREAM_API = `${API_BASE}/mcp/generate-story-stream`;
337
337
  const PROVIDERS_API = `${API_BASE}/mcp/providers`;
338
338
  const STORIES_API = `${API_BASE}/story-ui/stories`;
339
+ const ORPHAN_STORIES_API = `${API_BASE}/story-ui/orphan-stories`;
339
340
  const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
340
341
 
341
342
  function isEdgeMode(): boolean {
@@ -791,6 +792,8 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
791
792
  const [contextMenuId, setContextMenuId] = useState<string | null>(null);
792
793
  const [renamingChatId, setRenamingChatId] = useState<string | null>(null);
793
794
  const [renameValue, setRenameValue] = useState('');
795
+ const [orphanCount, setOrphanCount] = useState<number>(0);
796
+ const [isDeletingOrphans, setIsDeletingOrphans] = useState<boolean>(false);
794
797
  const chatEndRef = useRef<HTMLDivElement>(null);
795
798
  const inputRef = useRef<HTMLInputElement>(null);
796
799
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -1492,6 +1495,62 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1492
1495
  }
1493
1496
  };
1494
1497
 
1498
+ // Check for orphan stories (stories without associated chats)
1499
+ const checkOrphanStories = useCallback(async () => {
1500
+ if (!state.connectionStatus.connected) return;
1501
+ try {
1502
+ const chatFileNames = state.recentChats.map(chat => chat.fileName);
1503
+ const response = await fetch(ORPHAN_STORIES_API, {
1504
+ method: 'POST',
1505
+ headers: { 'Content-Type': 'application/json' },
1506
+ body: JSON.stringify({ chatFileNames }),
1507
+ });
1508
+ if (response.ok) {
1509
+ const data = await response.json();
1510
+ setOrphanCount(data.count || 0);
1511
+ }
1512
+ } catch (error) {
1513
+ console.error('Failed to check orphan stories:', error);
1514
+ }
1515
+ }, [state.connectionStatus.connected, state.recentChats]);
1516
+
1517
+ // Delete all orphan stories
1518
+ const handleDeleteOrphans = async () => {
1519
+ if (orphanCount === 0) return;
1520
+ if (!confirm(`Delete ${orphanCount} orphan ${orphanCount === 1 ? 'story' : 'stories'}? These are generated story files without associated chats.`)) {
1521
+ return;
1522
+ }
1523
+ setIsDeletingOrphans(true);
1524
+ try {
1525
+ const chatFileNames = state.recentChats.map(chat => chat.fileName);
1526
+ const response = await fetch(ORPHAN_STORIES_API, {
1527
+ method: 'DELETE',
1528
+ headers: { 'Content-Type': 'application/json' },
1529
+ body: JSON.stringify({ chatFileNames }),
1530
+ });
1531
+ if (response.ok) {
1532
+ const data = await response.json();
1533
+ setOrphanCount(0);
1534
+ if (data.count > 0) {
1535
+ // Show success message briefly
1536
+ alert(`Deleted ${data.count} orphan ${data.count === 1 ? 'story' : 'stories'}.`);
1537
+ }
1538
+ } else {
1539
+ alert('Failed to delete orphan stories. Please try again.');
1540
+ }
1541
+ } catch (error) {
1542
+ console.error('Failed to delete orphan stories:', error);
1543
+ alert('Failed to delete orphan stories. Please try again.');
1544
+ } finally {
1545
+ setIsDeletingOrphans(false);
1546
+ }
1547
+ };
1548
+
1549
+ // Check for orphans when chats change or connection is established
1550
+ useEffect(() => {
1551
+ checkOrphanStories();
1552
+ }, [checkOrphanStories]);
1553
+
1495
1554
  const handleStartRename = (chatId: string, currentTitle: string, e?: React.MouseEvent) => {
1496
1555
  if (e) e.stopPropagation();
1497
1556
  setContextMenuId(null);
@@ -1677,6 +1736,30 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1677
1736
  ))}
1678
1737
  </div>
1679
1738
 
1739
+ {/* Orphan Stories Footer */}
1740
+ {orphanCount > 0 && (
1741
+ <div className="sui-orphan-footer">
1742
+ <button
1743
+ className="sui-orphan-delete-btn"
1744
+ onClick={handleDeleteOrphans}
1745
+ disabled={isDeletingOrphans}
1746
+ title={`${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`}
1747
+ >
1748
+ {isDeletingOrphans ? (
1749
+ <>
1750
+ <span className="sui-orphan-spinner" />
1751
+ <span>Deleting...</span>
1752
+ </>
1753
+ ) : (
1754
+ <>
1755
+ {Icons.trash}
1756
+ <span>{orphanCount} orphan {orphanCount === 1 ? 'story' : 'stories'}</span>
1757
+ </>
1758
+ )}
1759
+ </button>
1760
+ </div>
1761
+ )}
1762
+
1680
1763
  </div>
1681
1764
  )}
1682
1765
  {!state.sidebarOpen && (