+ {/* Page Header */}
+
+
Dashboard
+
Overview of your agent's status.
+
- {/* Row 1: Status + Chat (Left) | Agent Settings (Right) */}
-
- {/* Left: Status + Chat */}
-
- {/* Status */}
+ {/* Status Section */}
+
{agentReady ? (
@@ -188,470 +59,42 @@ export default function HomePage() {
? 'text-green-600 dark:text-green-400'
: 'text-yellow-600 dark:text-yellow-400'
)}>
- {agentReady
- ? 'Your agent is running'
- : 'Configure LLM provider to start'}
+ {agentReady ? 'Your agent is running' : 'Configure LLM provider to start'}
{agentReady
? 'Ready to assist you. Start a conversation to get things done.'
- : 'Select an LLM provider on the right to enable your agent.'}
+ : 'Go to Agent settings to configure your LLM provider.'}
-
navigate('/chat')}
- disabled={!agentReady}
- >
- Start Chat
-
-
-
-
- {/* Vertical Divider */}
-
-
- {/* Right: Agent Settings (stacked vertically) */}
-
- {/* Agent Profile */}
-
-
Agent Profile
+
setSettingsOpen(true)}
+ size="lg"
+ className="gap-2"
+ onClick={() => navigate('/chat')}
+ disabled={!agentReady}
>
- {agentName || 'Unnamed Agent'}
-
-
-
-
- {/* LLM Provider */}
-
-
LLM Provider
-
setProviderDropdownOpen(!providerDropdownOpen)}
- disabled={providerLoading || switching}
- >
-
- {current?.available ? (
-
- ) : (
-
- )}
- {current?.providerName ?? 'Loading...'}
- ·
- {current?.model ?? '-'}
-
-
+ Start Chat
+
- {/* Provider Dropdown */}
- {providerDropdownOpen && (
-
-
- {providers.map((p) => (
- {
- if (!p.available) {
- setSelectedProvider({
- id: p.id,
- name: p.name,
- authMethod: p.authMethod,
- loginCommand: p.loginCommand,
- })
- setProviderDropdownOpen(false)
- if (p.authMethod === 'oauth') {
- setOauthDialogOpen(true)
- } else {
- setApiKeyDialogOpen(true)
- }
- return
- }
- setSwitching(true)
- setProviderDropdownOpen(false)
- const result = await setProvider(p.id)
- setSwitching(false)
- if (!result.ok) {
- console.error('Failed to switch provider:', result.error)
- }
- }}
- disabled={switching}
- >
-
-
- {p.id === 'claude-code' ? 'Claude Code' :
- p.id === 'openai-codex' ? 'Codex' :
- p.id === 'kimi-coding' ? 'Kimi' :
- p.id === 'anthropic' ? 'Anthropic' :
- p.id === 'openai' ? 'OpenAI' :
- p.id === 'openrouter' ? 'OpenRouter' :
- p.name.split(' ')[0]}
-
-
- ))}
-
-
- {/* Model List */}
- {(() => {
- const currentProvider = providers.find(p => p.id === current?.provider)
- if (!currentProvider || currentProvider.models.length <= 1) return null
- return (
-
-
- Models
-
-
- {currentProvider.models.map((model) => (
- {
- if (model === current?.model) return
- setSwitching(true)
- setProviderDropdownOpen(false)
- const result = await setProvider(currentProvider.id, model)
- setSwitching(false)
- if (!result.ok) {
- console.error('Failed to switch model:', result.error)
- }
- }}
- disabled={switching}
- >
-
- {model}
-
- ))}
-
-
- )
- })()}
-
+ {!agentReady && (
+
navigate('/agent/profile')}
+ >
+
+ Configure Agent
+
)}
-
- {/* Divider */}
-
-
- {/* Section 3: Capabilities (Collapsible) */}
-
-
-
-
- Your agent currently has {capabilitySummary}
-
-
- {capabilitiesRefreshing ? (
-
- ) : (
-
- )}
-
- Refresh status
-
-
-
- {capabilitiesOpen ? 'Hide' : 'Details'}
-
-
-
-
-
- {/* Skills */}
-
-
-
-
-
Skills ({skillStats.enabled})
-
-
navigate('/skills')}
- >
- View all
-
-
-
- {skillStats.enabled > 0 ? (
-
- {skills.filter(s => s.enabled).slice(0, 8).map((skill) => (
-
- {skill.name}
-
- ))}
- {skillStats.enabled > 8 && (
-
- +{skillStats.enabled - 8} more
-
- )}
-
- ) : (
-
No skills enabled
- )}
-
-
- {/* Tools */}
-
-
-
-
- Tools ({enabledTools})
-
-
navigate('/tools')}
- >
- View all
-
-
-
- {enabledTools > 0 ? (
-
- {tools.filter(t => t.enabled).slice(0, 8).map((tool) => (
-
- {tool.name}
-
- ))}
- {enabledTools > 8 && (
-
- +{enabledTools - 8} more
-
- )}
-
- ) : (
-
No tools enabled
- )}
-
-
- {/* Channels */}
-
-
-
-
- Channels ({connectedChannels})
-
-
navigate('/channels')}
- >
- View all
-
-
-
- {connectedChannels > 0 ? (
-
- {channelStates.filter(s => s.status === 'running').slice(0, 8).map((channel) => (
-
- {channel.channelId}/{channel.accountId}
-
- ))}
- {connectedChannels > 8 && (
-
- +{connectedChannels - 8} more
-
- )}
-
- ) : (
-
No channels connected
- )}
-
-
- {/* Cron Jobs */}
-
-
-
-
- Scheduled Tasks ({cronCount})
-
-
navigate('/crons')}
- >
- View all
-
-
-
- {cronCount > 0 ? (
-
- {cronJobs.slice(0, 8).map((job) => (
-
- {job.name}
-
- ))}
- {cronCount > 8 && (
-
- +{cronCount - 8} more
-
- )}
-
- ) : (
-
No scheduled tasks
- )}
-
-
-
-
- {/* Divider */}
-
-
- {/* Section 4: Multi-Device Access */}
-
-
- {/* Left: Connect */}
-
-
-
Control from Anywhere
- setQrCodeExpanded(!qrCodeExpanded)}
- >
-
- {qrCodeExpanded ? 'Hide' : 'Show'}
-
-
-
- Open Multica Web on your phone and scan. Operate your computer and use your agent remotely.
-
-
- {/* QR Code Container */}
-
- {qrCodeExpanded ? (
-
- ) : (
- setQrCodeExpanded(true)}
- className="flex flex-col items-center justify-center gap-3 p-8 rounded-xl border-2 border-dashed border-muted-foreground/25 hover:border-muted-foreground/50 transition-colors cursor-pointer"
- >
-
- Click to show QR code
-
- )}
-
-
-
- {/* Vertical Divider */}
-
-
- {/* Right: Authorized Devices */}
-
-
Authorized Devices
-
- Devices you've approved to access your agent.
-
-
-
-
-
-
-
-
- {/* Dialogs */}
-
-
- {selectedProvider && selectedProvider.authMethod === 'api-key' && (
-
{
- await refresh()
- const result = await setProvider(selectedProvider.id)
- if (!result.ok) {
- console.error('Failed to switch provider:', result.error)
- }
- }}
- />
- )}
-
- {selectedProvider && selectedProvider.authMethod === 'oauth' && (
- {
- await refresh()
- const result = await setProvider(selectedProvider.id)
- if (!result.ok) {
- console.error('Failed to switch provider:', result.error)
- }
- }}
- />
- )}
-
)
}
diff --git a/apps/desktop/src/renderer/src/pages/layout.tsx b/apps/desktop/src/renderer/src/pages/layout.tsx
index 3a06a9eb..5129b0e2 100644
--- a/apps/desktop/src/renderer/src/pages/layout.tsx
+++ b/apps/desktop/src/renderer/src/pages/layout.tsx
@@ -1,26 +1,33 @@
import { Outlet, NavLink, useLocation, useNavigate } from 'react-router-dom'
import { Button } from '@multica/ui/components/ui/button'
import { MulticaIcon } from '@multica/ui/components/multica-icon'
+import {
+ Collapsible,
+ CollapsibleTrigger,
+ CollapsibleContent,
+} from '@multica/ui/components/ui/collapsible'
import {
Home,
MessageSquare,
- Puzzle,
- Wrench,
- Radio,
+ Users,
Clock,
ChevronLeft,
ChevronRight,
+ ChevronDown,
+ Bot,
} from 'lucide-react'
import {
Sidebar,
SidebarContent,
SidebarGroup,
- SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
SidebarProvider,
SidebarTrigger,
useSidebar,
@@ -31,23 +38,30 @@ import { DeviceConfirmDialog } from '../components/device-confirm-dialog'
import { UpdateNotification } from '../components/update-notification'
const mainNavItems = [
- { path: '/', label: 'Home', icon: Home },
+ { path: '/', label: 'Home', icon: Home, exact: true },
{ path: '/chat', label: 'Chat', icon: MessageSquare },
]
-const configNavItems = [
- { path: '/skills', label: 'Skills', icon: Puzzle },
- { path: '/tools', label: 'Tools', icon: Wrench },
- { path: '/channels', label: 'Channels', icon: Radio },
- { path: '/crons', label: 'Crons', icon: Clock },
+const agentSubItems = [
+ { path: '/agent/profile', label: 'Profile' },
+ { path: '/agent/skills', label: 'Skills' },
+ { path: '/agent/tools', label: 'Tools' },
+]
+
+const bottomNavItems = [
+ { path: '/clients', label: 'Clients', icon: Users },
+ { path: '/crons', label: 'Scheduled Tasks', icon: Clock },
]
// All nav items for header lookup
-const allNavItems = [...mainNavItems, ...configNavItems]
+const allNavItems: Array<{ path: string; label: string; icon: typeof Home; exact?: boolean }> = [
+ ...mainNavItems,
+ { path: '/agent', label: 'Agent', icon: Bot },
+ ...bottomNavItems,
+]
function NavigationButtons() {
const navigate = useNavigate()
- // useLocation() triggers re-render on route change so we can re-evaluate history state
useLocation()
const historyIdx = window.history.state?.idx ?? 0
@@ -84,27 +98,29 @@ function MainHeader() {
const location = useLocation()
const needsTrafficLightSpace = state === 'collapsed' || isMobile
- // Find current page info
- const currentPage = allNavItems.find((item) =>
- item.path === '/'
- ? location.pathname === '/'
- : location.pathname.startsWith(item.path)
- )
+ const currentPage = allNavItems.find((item) => {
+ if (item.exact) return location.pathname === item.path
+ return location.pathname.startsWith(item.path)
+ })
return (
-
- {/* Drag placeholder for traffic lights when sidebar is collapsed */}
+
-
+
+
+
- {/* Center: Current page */}
{currentPage && (
@@ -114,95 +130,120 @@ function MainHeader() {
)}
- {/* Right: Theme toggle */}
-
+
+
+
)
}
export default function Layout() {
const location = useLocation()
+ const isAgentActive = location.pathname.startsWith('/agent')
return (
- {/* Traffic light area with navigation */}
-
-
-
+
+
+
-
- {/* Brand */}
-
-
- Multica
-
+
+
+
+ Multica
+
- {/* Main navigation */}
-
-
- {mainNavItems.map((item) => {
- const isActive = item.path === '/'
- ? location.pathname === '/'
- : location.pathname.startsWith(item.path)
- return (
-
-
-
-
- {item.label}
-
-
+
+
+ {/* Main nav items */}
+ {mainNavItems.map((item) => {
+ const isActive = item.exact
+ ? location.pathname === item.path
+ : location.pathname.startsWith(item.path)
+ return (
+
+
+
+
+ {item.label}
+
+
+
+ )
+ })}
+
+ {/* Agent collapsible */}
+
+
+ }
+ >
+
+ Agent
+
+
+
+
+ {agentSubItems.map((item) => (
+
+ }
+ isActive={location.pathname === item.path}
+ >
+ {item.label}
+
+
+ ))}
+
+
- )
- })}
-
-
+
- {/* Configuration */}
-
- Agent Configuration
-
- {configNavItems.map((item) => {
- const isActive = location.pathname.startsWith(item.path)
- return (
-
-
-
-
- {item.label}
-
-
-
- )
- })}
-
-
-
-
+ {/* Bottom nav items */}
+ {bottomNavItems.map((item) => {
+ const isActive = location.pathname.startsWith(item.path)
+ return (
+
+
+
+
+ {item.label}
+
+
+
+ )
+ })}
+
+
+
+
-
-
-
- {/* Main Content */}
-
-
-
-
+
+
+
+
+
+
diff --git a/apps/desktop/src/renderer/src/pages/skills.tsx b/apps/desktop/src/renderer/src/pages/skills.tsx
deleted file mode 100644
index 778ff386..00000000
--- a/apps/desktop/src/renderer/src/pages/skills.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useSkillsStore } from '../stores/skills'
-import { SkillList } from '../components/skill-list'
-
-export default function SkillsPage() {
- const { skills, loading, error, toggleSkill, refresh } = useSkillsStore()
-
- return (
-
-
- {/* Page Header */}
-
-
Skills
-
- Skills are modular capabilities that expand what your agent can do. You can also ask your agent to create new skills for you.
-
-
-
- {/* Configuration Area */}
-
-
-
-
-
- )
-}
diff --git a/apps/desktop/src/renderer/src/pages/tools.tsx b/apps/desktop/src/renderer/src/pages/tools.tsx
deleted file mode 100644
index 19c7e96d..00000000
--- a/apps/desktop/src/renderer/src/pages/tools.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useToolsStore } from '../stores/tools'
-import { ToolList } from '../components/tool-list'
-
-export default function ToolsPage() {
- const { tools, loading, error, toggleTool, refresh } = useToolsStore()
-
- return (
-
-
- {/* Page Header */}
-
-
Tools
-
- Tools are actions your agent can perform, like reading files, searching the web, or running code. Toggle them to control what your agent can do.
-
-
-
- {/* Configuration Area */}
-
-
-
-
-
- )
-}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 69ea8aae..660b3dbc 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,10 +1,23 @@
import type { Metadata } from "next";
-import "@multica/ui/fonts";
+import { Geist_Mono, Inter, Playfair_Display } from "next/font/google";
import "@multica/ui/globals.css";
import { ThemeProvider } from "@multica/ui/components/theme-provider";
import { Toaster } from "@multica/ui/components/ui/sonner";
import { ServiceWorkerRegister } from "./sw-register";
+const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+const playfair = Playfair_Display({
+ variable: "--font-brand",
+ subsets: ["latin"],
+ weight: ["400"],
+});
+
export const metadata: Metadata = {
title: "Multica",
description: "Distributed AI agent framework",
@@ -24,8 +37,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
+
+
+ )
+}
+
+const tabsListVariants = cva(
+ "rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: TabsPrimitive.List.Props & VariantProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
+ return (
+
+ )
+}
+
+function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/packages/ui/src/styles/fonts.ts b/packages/ui/src/styles/fonts.ts
index a9694b2b..f2ed41b0 100644
--- a/packages/ui/src/styles/fonts.ts
+++ b/packages/ui/src/styles/fonts.ts
@@ -1,11 +1,5 @@
-// Unified font imports for Web and Desktop
-// Using fontsource for consistent cross-platform font loading
-
-// Geist Sans - Primary UI font
-import '@fontsource/geist-sans/400.css'
-import '@fontsource/geist-sans/500.css'
-import '@fontsource/geist-sans/600.css'
-import '@fontsource/geist-sans/700.css'
+// Font imports for Desktop (Electron)
+// Web uses next/font/google instead
// Geist Mono - Code font
import '@fontsource/geist-mono/400.css'
@@ -13,5 +7,5 @@ import '@fontsource/geist-mono/500.css'
import '@fontsource/geist-mono/600.css'
import '@fontsource/geist-mono/700.css'
-// Playfair Display - Brand font
-import '@fontsource-variable/playfair-display'
+// Note: Geist Sans removed - Desktop uses system fonts for CJK support
+// Note: Playfair Display loaded via Google Fonts in index.html
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index 4faacdc9..7c7f1cf1 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -18,24 +18,14 @@
* TYPOGRAPHY
* =============================================================================
*
- * Font Stack (loaded via @fontsource, works across Web + Desktop):
+ * Fonts are loaded via CSS variables injected by each app:
+ * - Web (Next.js): Uses next/font/google in layout.tsx
+ * - Desktop (Electron): Uses @fontsource packages
*
- * | Font | Variable | Usage |
- * |---------------------------|---------------|------------------------------|
- * | Geist Sans | font-sans | Primary UI text, body copy |
- * | Geist Mono | font-mono | Code, technical values |
- * | Playfair Display Variable | font-brand | Brand name "Multica" only |
- *
- * Why Geist?
- * - Created by Vercel, optimized for UI/developer tools
- * - Excellent legibility at small sizes (12-14px)
- * - Neutral, professional appearance — not "AI-ish" or trendy
- * - Variable font = smaller bundle, flexible weights
- *
- * Why Playfair Display for brand?
- * - Contrast with Geist creates clear hierarchy
- * - Serif adds warmth/personality to otherwise minimal UI
- * - Used ONLY for "Multica" text — nowhere else
+ * Variables used:
+ * - --font-sans: Primary UI text (Inter/system font)
+ * - --font-geist-mono: Code and technical values
+ * - --font-brand: Brand name "Multica" only (Playfair Display)
*
* =============================================================================
* COLOR SYSTEM
@@ -93,9 +83,9 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
- --font-mono: 'Geist Mono', ui-monospace, monospace;
- --font-brand: 'Playfair Display Variable', serif;
+ --font-sans: var(--font-sans);
+ --font-mono: var(--font-geist-mono);
+ --font-brand: var(--font-brand);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -135,14 +125,6 @@
}
:root {
- /* =========================================================================
- * FONTS — unified across Web (Next.js) and Desktop (Electron/Vite)
- * Loaded via @fontsource packages in packages/ui/src/styles/fonts.ts
- * ========================================================================= */
- --font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
- --font-mono: 'Geist Mono', ui-monospace, monospace;
- --font-brand: 'Playfair Display Variable', serif;
-
/* =========================================================================
* COLORS — Light mode
* Using OKLCH for perceptual uniformity across the palette
@@ -275,7 +257,7 @@
}
@utility container {
- @apply w-full max-w-4xl mx-auto;
+ @apply w-full max-w-5xl mx-auto;
}
@layer base {