import { type ReactNode } from 'react' import { IconAlertSquareRoundedFilled, IconPlus, IconSmartHome } from '@tabler/icons-react' import { useQueryClient } from '@tanstack/react-query' import { Link, useLocation } from 'wouter' import { useAppConfig } from '@/client/api/app-config' import { useReorderWorkspaces, useWorkspaces } from '@/client/features/home/api' import { Update } from '@/client/features/update/Update' import { workspaceKeys } from '@/client/api/workspace-keys' import { useWorkspaceEvent } from '@/client/runtime/useWorkspaceEvents' import { CreateWorkspaceDialog } from '@/client/features/home/workspace-setup/CreateWorkspaceDialog' import { DemoDialog } from '@/client/features/home/workspace-setup/DemoDialog' import { ReorderableList } from '@/client/components/shared/ReorderableList' import type { ReorderableRenderState } from '@/client/components/shared/ReorderableList' import { Button, buttonVariants } from '@/client/components/ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/client/components/ui/tooltip' import { workspaceDisplayName, workspaceProviderIcon } from '@/client/features/home/workspace-presentation' import { cn } from '@/client/lib/cn' import type { WorkspaceEntry } from '@/lib/types' function sidebarNavButtonClass(active: boolean): string { return cn( buttonVariants({ variant: 'ghost', size: 'icon' }), active && 'bg-accent text-accent-foreground' ) } type SidebarLayoutProps = { // Full panel content — the page supplies its own header (compose it with // ) and body. children?: ReactNode showWorkspaces?: boolean } // App shell: a slim icon rail beside the page content. The page owns the panel's // header and body. export function SidebarLayout({ children, showWorkspaces = true }: SidebarLayoutProps) { const { data: workspaces } = useWorkspaces() // `moi config` / the settings modal broadcast `workspace:updated` (identity // changes) and reorder/create broadcast `workspaces-list:updated`; refetch the // list so the sidebar reflects it live. Exact: `workspaceKeys.all` is the // prefix of every workspace query — a prefix invalidation would refetch // transcripts, widgets, and MCP probes in every connected client. const qc = useQueryClient() useWorkspaceEvent(e => { if (e.type === 'workspace:updated' || e.type === 'workspaces-list:updated') { qc.invalidateQueries({ queryKey: workspaceKeys.all, exact: true }) } }) return (
{children}
) } type SidebarProps = { workspaces: WorkspaceEntry[] } function Sidebar({ workspaces }: SidebarProps) { const reorder = useReorderWorkspaces() const { cloudDemo } = useAppConfig() return ( ) } type WorkspaceButtonProps = { workspace: WorkspaceEntry dragOverlay?: boolean dragState?: ReorderableRenderState } function WorkspaceButton({ workspace, dragOverlay = false, dragState }: WorkspaceButtonProps) { const [location] = useLocation() const href = `/workspace/${workspace.id}` const label = workspaceDisplayName(workspace) // A workspace URL carries a tab suffix (`/workspace/:id/view:orders`), so the // rail matches the workspace segment, not the whole path. The trailing slash // keeps `/workspace/ws1` from lighting up a sibling `/workspace/ws1-other`. const active = location === href || location.startsWith(`${href}/`) const icon = (
) const labelElement = ( {label} ) if (dragOverlay) { return ( {icon} {labelElement} ) } return ( { if (dragState?.isDragging) event.preventDefault() }} className={cn( 'group flex w-14 flex-col items-center rounded-lg outline-none', dragState?.isDragging && 'invisible' )} {...dragState?.dragHandleProps} > {icon} {label} ) }