import { useState, useCallback, useEffect, useRef } from 'react'; import { X, Plus, Users, Cloud, HardDrive, Server, FolderOpen, Folder, FolderPlus, ChevronRight, Home, Check, Loader2, LayoutTemplate, Sparkles, Info, Wand2, Target, Microscope, Code, Megaphone, Rocket, Scale, Building, FileText, Laptop, PenLine, BarChart3, ClipboardList, Mail, Plug, Terminal, Pencil, Trash2, Copy, Filter, Search, } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Input } from '@/components/ui/input'; import { HintTooltip } from '@/components/ui/hint-tooltip'; import { PERSONAS } from '@/lib/personas'; import { motion, AnimatePresence, useDragControls } from 'framer-motion'; import { adapter } from '@/lib/adapter'; import { useWorkspaces } from '@/hooks/useWorkspaces'; import { useShell } from '@/providers/ShellContext'; import { canCreateWorkspaceAtTier } from '@/lib/workspace-limit'; import LockedFeature from '@/components/os/LockedFeature'; import { buildBreadcrumbs } from '@/lib/browse-breadcrumbs'; import { useFocusTrap } from '@/hooks/useFocusTrap'; import type { StorageType, WorkspaceTemplate, TemplateCategory } from '@/lib/types'; import { TIER_CAPABILITIES, type ConnectorDefinition } from '@waggle/shared'; /** Use native OS folder picker when running inside Tauri, falls back to custom browse modal. */ async function pickFolderNative(): Promise { try { const { open } = await import('@tauri-apps/plugin-dialog'); const selected = await open({ directory: true, multiple: false, title: 'Choose workspace directory' }); return typeof selected === 'string' ? selected : null; } catch { return null; // Not in Tauri — fallback to custom picker } } /* ── Shared constants ─────────────────────────────────────────────── */ interface AgentGroupOption { id: string; name: string; description?: string; strategy: string; memberCount: number; } interface CreateWorkspaceDialogProps { open: boolean; onClose: () => void; onCreate: (data: { name: string; group: string; persona?: string; agentGroupId?: string; shared?: boolean; storageType?: StorageType; storagePath?: string; templateId?: string; }) => void; } import { STANDARD_GROUPS } from '@/lib/workspace-groups'; const GROUPS = [...STANDARD_GROUPS]; const STORAGE_OPTIONS: { type: StorageType; label: string; desc: string; icon: React.ElementType; color: string }[] = [ { type: 'virtual', label: 'Virtual', desc: 'Server-managed storage', icon: Cloud, color: 'text-violet-400' }, { type: 'local', label: 'Local', desc: 'Local disk directory', icon: HardDrive, color: 'text-emerald-400' }, { type: 'team', label: 'Team', desc: 'Remote S3/MinIO storage', icon: Server, color: 'text-sky-400' }, ]; /** Icon map for known template IDs */ const TEMPLATE_ICONS: Record = { 'sales-pipeline': Target, 'research-project': Microscope, 'code-review': Laptop, 'marketing-campaign': Megaphone, 'product-launch': Rocket, 'legal-review': Scale, 'agency-consulting': Building, }; /** Available slash commands in the system */ const AVAILABLE_COMMANDS = [ { id: '/research', label: '/research', desc: 'Deep-dive investigation' }, { id: '/draft', label: '/draft', desc: 'Write content & documents' }, { id: '/review', label: '/review', desc: 'Analyze & review' }, { id: '/plan', label: '/plan', desc: 'Create plans & checklists' }, { id: '/status', label: '/status', desc: 'Progress reports' }, { id: '/catchup', label: '/catchup', desc: 'Quick briefing' }, { id: '/memory', label: '/memory', desc: 'Search memory' }, { id: '/spawn', label: '/spawn', desc: 'Spawn sub-agents' }, ]; /** Available persona roles for templates */ const TEMPLATE_PERSONAS = [ { id: 'researcher', name: 'Researcher', icon: Microscope }, { id: 'writer', name: 'Writer', icon: PenLine }, { id: 'analyst', name: 'Analyst', icon: BarChart3 }, { id: 'coder', name: 'Coder', icon: Code }, { id: 'project-manager', name: 'Project Manager', icon: ClipboardList }, { id: 'executive-assistant', name: 'Executive Assistant', icon: Mail }, { id: 'sales-rep', name: 'Sales Rep', icon: Target }, { id: 'marketer', name: 'Marketer', icon: Megaphone }, ]; const TEMPLATE_CATEGORIES: { id: TemplateCategory | 'all'; label: string }[] = [ { id: 'all', label: 'All' }, { id: 'sales', label: 'Sales' }, { id: 'research', label: 'Research' }, { id: 'engineering', label: 'Engineering' }, { id: 'marketing', label: 'Marketing' }, { id: 'operations', label: 'Operations' }, { id: 'legal', label: 'Legal' }, { id: 'custom', label: 'Custom' }, ]; function defaultVirtualPath(workspaceName: string): string { const slug = workspaceName.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') || 'untitled'; return `/workspaces/${slug}`; } /* ── Tooltip ──────────────────────────────────────────────────────── */ function Tooltip({ text, children }: { text: string; children: React.ReactNode }) { const [show, setShow] = useState(false); return ( setShow(true)} onMouseLeave={() => setShow(false)}> {children} {show && ( {text} )} ); } /* ── Multi-Select Chip Picker ─────────────────────────────────────── */ interface ChipOption { id: string; label: string; desc?: string } function ChipPicker({ options, selected, onChange, label }: { options: ChipOption[]; selected: string[]; onChange: (ids: string[]) => void; label: string; }) { const toggle = (id: string) => { onChange(selected.includes(id) ? selected.filter(s => s !== id) : [...selected, id]); }; return (
{options.map(opt => { const isActive = selected.includes(opt.id); return ( ); })}
); } /* ── Browse Entry ─────────────────────────────────────────────────── */ interface BrowseEntry { name: string; path: string; type: string; } /* ── Folder Picker Modal (live API) ───────────────────────────────── */ interface FolderPickerProps { open: boolean; storageType: StorageType; currentPath: string; onSelect: (path: string) => void; onClose: () => void; } function FolderPickerModal({ open, storageType, currentPath, onSelect, onClose }: FolderPickerProps) { const rootLabel = storageType === 'local' ? '/' : 'Buckets'; const [browsePath, setBrowsePath] = useState(currentPath || '/'); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [newFolderName, setNewFolderName] = useState(''); const [showNewFolder, setShowNewFolder] = useState(false); const [creatingFolder, setCreatingFolder] = useState(false); const fetchEntries = useCallback(async (dirPath: string) => { setLoading(true); setError(null); try { if (storageType === 'local') { const result = await adapter.browseLocal(dirPath); setEntries(result.entries); } else { setEntries([]); setError('Team storage browsing is not yet available'); } } catch (err: any) { setError(err.message ?? 'Failed to browse'); setEntries([]); } finally { setLoading(false); } }, [storageType]); useEffect(() => { if (open) fetchEntries(browsePath); }, [open, browsePath, fetchEntries]); const handleNavigate = useCallback((dirPath: string) => { setBrowsePath(dirPath); }, []); const handleCreateFolder = useCallback(async () => { const folderName = newFolderName.trim(); if (!folderName) return; const newPath = browsePath.endsWith('/') ? `${browsePath}${folderName}` : `${browsePath}/${folderName}`; setCreatingFolder(true); try { if (storageType === 'local') await adapter.browseLocalMkdir(newPath); await fetchEntries(browsePath); setBrowsePath(newPath); setNewFolderName(''); setShowNewFolder(false); } catch (err: any) { setError(err.message ?? 'Failed to create folder'); } finally { setCreatingFolder(false); } }, [browsePath, newFolderName, storageType, fetchEntries]); // P14: Windows paths (C:\Users\Marko...) need separator-aware splitting. // The old inline logic split only on '/' and collapsed Windows paths into // a single unusable crumb. Pure helper handles both separator styles. const breadcrumbs = buildBreadcrumbs( browsePath, rootLabel, storageType as 'local' | 'virtual' | 'team', ); const dialogRef = useFocusTrap(open, onClose); const titleId = storageType === 'local' ? 'folder-picker-title' : 'bucket-picker-title'; if (!open) return null; return (