diff --git a/CLAUDE.md b/CLAUDE.md index 633454cb..a37195d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,43 +1,107 @@ -# Project Instructions for AI Agents +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Super Multica is a distributed AI agent framework with a monorepo architecture. It includes an agent engine with multi-provider LLM support, a WebSocket gateway, a console hub for multi-agent coordination, and frontend apps (Next.js web, Electron desktop). + +## Monorepo Structure + +- **`src/`** — Core modules (agent engine, gateway, console, client, shared types) +- **`apps/web`** — Next.js 16 web app (`@multica/web`, port 3001) +- **`apps/desktop`** — Electron + Vite + React desktop app (`@multica/desktop`) +- **`packages/ui`** — Shared UI component library (`@multica/ui`, Shadcn/Tailwind CSS v4) +- **`packages/sdk`** — Gateway client SDK (`@multica/sdk`, Socket.io) +- **`packages/store`** — Zustand state management (`@multica/store`) +- **`skills/`** — Bundled agent skills (commit, code-review, skill-creator) + +## Common Commands + +```bash +# Install dependencies +pnpm install + +# Development (all services concurrently: gateway:3000, console, web:3001) +pnpm dev + +# Individual services +pnpm dev:gateway # WebSocket gateway only +pnpm dev:console # NestJS console with agent +pnpm dev:web # Next.js web app +pnpm dev:desktop # Electron desktop app + +# Agent CLI +pnpm agent:cli # Non-interactive agent +pnpm agent:interactive # Interactive REPL mode + +# Build (turbo-orchestrated) +pnpm build + +# Type checking +pnpm typecheck + +# Testing (vitest, tests live in src/**/*.test.ts) +pnpm test # Single run +pnpm test:watch # Watch mode +pnpm test:coverage # With v8 coverage +``` + +## Architecture + +``` +Frontend (web:3001 / desktop) + → @multica/sdk (GatewayClient, Socket.io) + → Gateway (NestJS, WebSocket, port 3000) + → Console Hub (multi-agent coordination) + → Agent Engine (LLM runner, sessions, skills, tools) +``` + +**Agent Engine** (`src/agent/`): Orchestrates LLM interactions with multi-provider support (OpenAI, Anthropic, DeepSeek, Kimi, Groq, Mistral, Google, Together). Features session management (JSONL-based, UUIDv7 IDs), profile system (`~/.super-multica/agent-profiles/`), modular skills with hot-reload, and token-aware context window guards (compaction modes: tokens, count, summary). + +**Gateway** (`src/gateway/`): NestJS WebSocket server with Socket.io for real-time message passing, RPC request/response, and streaming. + +**Console** (`src/console/`): NestJS hub for multi-agent coordination with a web dashboard. + +## Tech Stack & Config + +- **Package manager**: pnpm 10 with workspaces (`pnpm-workspace.yaml`) +- **Build orchestration**: Turborepo (`turbo.json`) +- **TypeScript**: ESNext target, NodeNext modules, strict mode, `verbatimModuleSyntax`, `experimentalDecorators` (NestJS) +- **Testing**: Vitest with globals enabled, node environment +- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI (zinc base, hugeicons) +- **Backend**: NestJS 11, Socket.io, Pino logging +- **CLI bundling**: esbuild → `bin/` directory + +## Environment Setup + +```bash +cp .env.example .env +# Set LLM_PROVIDER and corresponding API keys +# Supported: openai, anthropic, deepseek, kimi-coding, groq, mistral, together, google +# .env is loaded automatically via --env-file in all dev/agent scripts +``` ## Atomic Commits -After completing any task that modifies code, you MUST create atomic commits before ending the conversation. Do not ask for permission - just do it. +After completing any task that modifies code, you MUST create atomic commits before ending the conversation. -### Workflow +1. Run `git status` and `git diff` to see all modifications +2. Skip if no changes exist +3. Group changes by logical purpose (feature, fix, refactor, docs, test, chore) +4. Stage and commit each group separately -1. **Check for changes**: Run `git status` and `git diff` to see all modifications -2. **Skip if clean**: If there are no changes, skip the commit process -3. **Analyze changes**: Group changes by their logical purpose: - - Feature additions - - Bug fixes - - Refactoring - - Documentation - - Tests - - Configuration/dependencies -4. **Create atomic commits**: For each logical group, stage only the relevant files and create a separate commit +**Format**: Conventional commits — `(): ` -### Commit Process +Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore` -For each logical group of changes: - -```bash -# Stage specific files for this logical change -git add - -# Commit with conventional commit message -git commit -m "(): " -``` - -### Commit Message Format - -Use conventional commits: -- `feat`: New feature -- `fix`: Bug fix -- `refactor`: Code refactoring (no functional change) -- `docs`: Documentation changes -- `test`: Adding or updating tests -- `chore`: Build, config, dependencies +**Rules**: +- Each commit should be independently meaningful and buildable +- Related test files go with their implementation +- Never create empty commits or combine unrelated changes +- If all changes are related to one logical unit, a single commit is fine +- Keep commit messages concise but descriptive +- `git commit --amend` only for immediate small fixes to the last commit ### Examples @@ -51,13 +115,3 @@ Create three commits: 1. `git add src/api/user.ts src/api/user.test.ts && git commit -m "feat(api): add user profile endpoint"` 2. `git add src/utils/format.ts && git commit -m "refactor(utils): simplify date formatting logic"` 3. `git add README.md && git commit -m "docs: update API documentation"` - -### Rules - -- Each commit should be independently meaningful and buildable -- Related test files should be committed with their implementation -- Never create empty commits -- Never combine unrelated changes in one commit -- Keep commit messages concise but descriptive -- If all changes are related to one logical unit, a single commit is fine -- `git commit --amend` can be used for immediate small fixes to the last commit, but not for unrelated changes diff --git a/apps/web/app/components/chat.tsx b/apps/web/app/components/chat.tsx new file mode 100644 index 00000000..6827cd23 --- /dev/null +++ b/apps/web/app/components/chat.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useRef, useState, useCallback } from "react"; +import { SidebarTrigger } from "@multica/ui/components/ui/sidebar"; +import { Badge } from "@multica/ui/components/ui/badge"; +import { Button } from "@multica/ui/components/ui/button"; +import { ChatInput } from "@multica/ui/components/chat-input"; +import { MemoizedMarkdown } from "@multica/ui/components/markdown"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { UserIcon, Copy01Icon, CheckmarkCircle02Icon } from "@hugeicons/core-free-icons"; +import { toast } from "@multica/ui/components/ui/sonner"; +import { useMessages } from "../hooks/use-messages"; +import { useGateway } from "../hooks/use-gateway"; +import { useHubStore } from "../hooks/use-hub-store"; +import { useDeviceId } from "../hooks/use-device-id"; +import { useScrollFade } from "../hooks/use-scroll-fade"; +import { cn } from "@multica/ui/lib/utils"; + +const STATE_VARIANT: Record = { + registered: "default", + connected: "secondary", + connecting: "secondary", + disconnected: "destructive", +} + +export function Chat() { + const activeAgentId = useHubStore((s) => s.activeAgentId) + const hub = useHubStore((s) => s.hub) + const { messages, addUserMessage, addAssistantMessage } = useMessages() + + const { state: gwState, send } = useGateway({ + onMessage: (msg) => { + const payload = msg.payload as { agentId?: string; content?: string } + if (payload?.agentId && payload?.content) { + addAssistantMessage(payload.content, payload.agentId) + } + }, + }) + + const handleSend = (text: string) => { + if (!hub?.hubId || !activeAgentId) return + addUserMessage(text, activeAgentId) + send(hub.hubId, "message", { agentId: activeAgentId, content: text }) + } + + const filtered = activeAgentId + ? messages.filter(m => m.agentId === activeAgentId) + : [] + + const canSend = gwState === "registered" && !!activeAgentId + + const deviceId = useDeviceId() + const [deviceCopied, setDeviceCopied] = useState(false) + const handleCopyDevice = useCallback(async () => { + if (!deviceId) return + await navigator.clipboard.writeText(deviceId) + setDeviceCopied(true) + toast.success("Device ID copied") + setTimeout(() => setDeviceCopied(false), 2000) + }, [deviceId]) + + const mainRef = useRef(null) + const fadeStyle = useScrollFade(mainRef) + + return ( +
+
+ + {deviceId && ( + <> + + {deviceId} + + + + )} + + {gwState} + +
+ +
+ {!activeAgentId ? ( +
+ + Select an agent to start chatting +
+ ) : filtered.length === 0 ? ( +
+ Send a message to start the conversation +
+ ) : ( +
+ {filtered.map((msg) => ( +
+
+ + {msg.content} + +
+
+ ))} +
+ )} +
+ +
+ +
+
+ ); +} diff --git a/apps/web/app/components/hub-sidebar.tsx b/apps/web/app/components/hub-sidebar.tsx new file mode 100644 index 00000000..6a958ba3 --- /dev/null +++ b/apps/web/app/components/hub-sidebar.tsx @@ -0,0 +1,110 @@ +"use client" + +import { + SidebarGroup, + SidebarGroupLabel, + SidebarGroupAction, + SidebarGroupContent, + SidebarMenu, + SidebarMenuItem, +} from "@multica/ui/components/ui/sidebar" +import { Button } from "@multica/ui/components/ui/button" +import { HugeiconsIcon } from "@hugeicons/react" +import { PlusSignIcon, Delete02Icon } from "@hugeicons/core-free-icons" +import { useHubStore } from "../hooks/use-hub-store" +import { useHubInit } from "../hooks/use-hub-init" + +const STATUS_DOT: Record = { + connected: "bg-green-500/60", + loading: "bg-yellow-500/50 animate-pulse", + error: "bg-red-500/60", + idle: "bg-muted-foreground/50", +} + +const STATUS_LABEL: Record = { + connected: "Connected", + loading: "Connecting...", + error: "Disconnected", + idle: "Idle", +} + +export function HubSidebar() { + useHubInit() + + const status = useHubStore((s) => s.status) + const hub = useHubStore((s) => s.hub) + const agents = useHubStore((s) => s.agents) + const activeAgentId = useHubStore((s) => s.activeAgentId) + const fetchHub = useHubStore((s) => s.fetchHub) + const createAgent = useHubStore((s) => s.createAgent) + const deleteAgent = useHubStore((s) => s.deleteAgent) + const setActiveAgentId = useHubStore((s) => s.setActiveAgentId) + + return ( + <> + + Hub + +
+ + {STATUS_LABEL[status]} +
+ {status === "connected" && hub && ( +
+ {hub.hubId} +
+ )} + {status === "error" && ( +
+ +
+ )} +
+
+ + {status === "connected" && ( + + Agents + + + + + + {agents.length === 0 && ( +
+ No agents +
+ )} + {agents.map(agent => ( + +
setActiveAgentId(agent.id)} + data-active={agent.id === activeAgentId || undefined} + className="flex items-center w-full h-8 px-2 rounded-md cursor-pointer hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-active:font-medium" + > + + {agent.id} + + +
+
+ ))} +
+
+
+ )} + + ) +} diff --git a/apps/web/app/favicon.ico b/apps/web/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/apps/web/app/favicon.ico and /dev/null differ diff --git a/apps/web/app/hooks/use-device-id.ts b/apps/web/app/hooks/use-device-id.ts new file mode 100644 index 00000000..842d4551 --- /dev/null +++ b/apps/web/app/hooks/use-device-id.ts @@ -0,0 +1,26 @@ +import { useSyncExternalStore } from "react" +import { v7 as uuidv7 } from "uuid" + +const STORAGE_KEY = "multica-device-id" + +function getSnapshot(): string { + let id = localStorage.getItem(STORAGE_KEY) + if (!id) { + id = uuidv7() + localStorage.setItem(STORAGE_KEY, id) + } + return id +} + +function subscribe(cb: () => void) { + window.addEventListener("storage", cb) + return () => window.removeEventListener("storage", cb) +} + +function getServerSnapshot(): string { + return "" +} + +export function useDeviceId(): string { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) +} diff --git a/apps/web/app/hooks/use-gateway.ts b/apps/web/app/hooks/use-gateway.ts new file mode 100644 index 00000000..bda4fa0f --- /dev/null +++ b/apps/web/app/hooks/use-gateway.ts @@ -0,0 +1,44 @@ +import { useEffect, useRef, useState, useCallback } from "react" +import { GatewayClient, type ConnectionState, type RoutedMessage } from "@multica/sdk" +import { useDeviceId } from "./use-device-id" +import { GATEWAY_URL } from "../lib/config" + +interface UseGatewayOptions { + onMessage?: (msg: RoutedMessage) => void +} + +export function useGateway(options?: UseGatewayOptions) { + const deviceId = useDeviceId() + const [state, setState] = useState("disconnected") + const clientRef = useRef(null) + const onMessageRef = useRef(options?.onMessage) + + useEffect(() => { + onMessageRef.current = options?.onMessage + }) + + useEffect(() => { + if (!deviceId) return + + const client = new GatewayClient({ + url: GATEWAY_URL, + deviceId, + deviceType: "client", + }) + .onStateChange(setState) + .onMessage(msg => onMessageRef.current?.(msg)) + + clientRef.current = client + client.connect() + return () => { client.disconnect() } + }, [deviceId]) + + const send = useCallback( + (to: string, action: string, payload: unknown) => { + clientRef.current?.send(to, action, payload) + }, + [] + ) + + return { state, send } +} diff --git a/apps/web/app/hooks/use-hub-init.ts b/apps/web/app/hooks/use-hub-init.ts new file mode 100644 index 00000000..9e988ebe --- /dev/null +++ b/apps/web/app/hooks/use-hub-init.ts @@ -0,0 +1,19 @@ +"use client" + +import { useEffect } from "react" +import { useHubStore } from "./use-hub-store" + +export function useHubInit() { + const fetchHub = useHubStore((s) => s.fetchHub) + const status = useHubStore((s) => s.status) + const fetchAgents = useHubStore((s) => s.fetchAgents) + + useEffect(() => { fetchHub() }, [fetchHub]) + useEffect(() => { + if (status === "connected") fetchAgents() + }, [status, fetchAgents]) + useEffect(() => { + const id = setInterval(fetchHub, 30_000) + return () => clearInterval(id) + }, [fetchHub]) +} diff --git a/apps/web/app/hooks/use-hub-store.ts b/apps/web/app/hooks/use-hub-store.ts new file mode 100644 index 00000000..010a1418 --- /dev/null +++ b/apps/web/app/hooks/use-hub-store.ts @@ -0,0 +1,75 @@ +import { create } from "zustand" +import { CONSOLE_URL } from "../lib/config" + +interface HubInfo { + hubId: string + url: string + connectionState: string + agentCount: number +} + +interface Agent { + id: string + closed: boolean +} + +type HubStatus = "idle" | "loading" | "connected" | "error" + +interface HubStore { + status: HubStatus + hub: HubInfo | null + agents: Agent[] + activeAgentId: string | null + + setActiveAgentId: (id: string | null) => void + fetchHub: () => Promise + fetchAgents: () => Promise + createAgent: () => Promise + deleteAgent: (id: string) => Promise +} + +export const useHubStore = create()((set, get) => ({ + status: "idle", + hub: null, + agents: [], + activeAgentId: null, + + setActiveAgentId: (id) => set({ activeAgentId: id }), + + fetchHub: async () => { + set({ status: "loading" }) + try { + const res = await fetch(`${CONSOLE_URL}/api/hub`) + if (!res.ok) throw new Error(res.statusText) + const data: HubInfo = await res.json() + set({ + hub: data, + status: data.connectionState === "registered" ? "connected" : "error", + }) + } catch { + set({ status: "error", hub: null }) + } + }, + + fetchAgents: async () => { + try { + const res = await fetch(`${CONSOLE_URL}/api/agents`) + if (res.ok) set({ agents: await res.json() }) + } catch { /* silent */ } + }, + + createAgent: async () => { + const res = await fetch(`${CONSOLE_URL}/api/agents`, { method: "POST" }) + await get().fetchAgents() + if (res.ok) { + const data = await res.json() + if (data.id) set({ activeAgentId: data.id }) + } + }, + + deleteAgent: async (id) => { + if (get().activeAgentId === id) set({ activeAgentId: null }) + await fetch(`${CONSOLE_URL}/api/agents/${id}`, { method: "DELETE" }) + await get().fetchAgents() + }, +})) diff --git a/apps/web/app/hooks/use-messages.ts b/apps/web/app/hooks/use-messages.ts new file mode 100644 index 00000000..8982b0d4 --- /dev/null +++ b/apps/web/app/hooks/use-messages.ts @@ -0,0 +1,25 @@ +import { useState, useCallback } from "react" +import { v7 as uuidv7 } from "uuid" + +export interface Message { + id: string + role: "user" | "assistant" + content: string + agentId: string +} + +export function useMessages() { + const [messages, setMessages] = useState([]) + + const addUserMessage = useCallback((content: string, agentId: string) => { + setMessages(prev => [...prev, { id: uuidv7(), role: "user", content, agentId }]) + }, []) + + const addAssistantMessage = useCallback((content: string, agentId: string) => { + setMessages(prev => [...prev, { id: uuidv7(), role: "assistant", content, agentId }]) + }, []) + + const clearMessages = useCallback(() => setMessages([]), []) + + return { messages, addUserMessage, addAssistantMessage, clearMessages } +} diff --git a/apps/web/app/hooks/use-scroll-fade.ts b/apps/web/app/hooks/use-scroll-fade.ts new file mode 100644 index 00000000..82709871 --- /dev/null +++ b/apps/web/app/hooks/use-scroll-fade.ts @@ -0,0 +1,68 @@ +import { type RefObject, type CSSProperties, useEffect, useState, useCallback } from "react"; + +/** + * Returns a dynamic maskImage style based on scroll position. + * - At top → fade bottom only + * - At bottom → fade top only + * - In middle → fade both + * - No overflow → undefined (no mask) + */ +export function useScrollFade( + ref: RefObject, + fadeSize = 32 +): CSSProperties | undefined { + const [fade, setFade] = useState<"none" | "top" | "bottom" | "both">("none"); + + const update = useCallback(() => { + const el = ref.current; + if (!el) return; + + const { scrollTop, scrollHeight, clientHeight } = el; + const scrollable = scrollHeight - clientHeight; + + if (scrollable <= 0) { + setFade("none"); + return; + } + + const atTop = scrollTop <= 1; + const atBottom = scrollTop >= scrollable - 1; + + if (atTop && atBottom) setFade("none"); + else if (atTop) setFade("bottom"); + else if (atBottom) setFade("top"); + else setFade("both"); + }, [ref]); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const frame = requestAnimationFrame(update); + + el.addEventListener("scroll", update, { passive: true }); + const ro = new ResizeObserver(update); + ro.observe(el); + + return () => { + cancelAnimationFrame(frame); + el.removeEventListener("scroll", update); + ro.disconnect(); + }; + }, [ref, update]); + + if (fade === "none") return undefined; + + const top = fade === "top" || fade === "both" ? `transparent 0%, black ${fadeSize}px` : "black 0%"; + const bottom = + fade === "bottom" || fade === "both" + ? `black calc(100% - ${fadeSize}px), transparent 100%` + : "black 100%"; + + const gradient = `linear-gradient(to bottom, ${top}, ${bottom})`; + + return { + maskImage: gradient, + WebkitMaskImage: gradient, + }; +} diff --git a/apps/web/app/icon.png b/apps/web/app/icon.png new file mode 100644 index 00000000..ba5cedcb Binary files /dev/null and b/apps/web/app/icon.png differ diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 8c345796..579809da 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,8 +1,16 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono, Inter } from "next/font/google"; +import { Geist, Geist_Mono, Inter, Playfair_Display } from "next/font/google"; import "@multica/ui/globals.css"; +import { + SidebarProvider, + SidebarInset, +} from "@multica/ui/components/ui/sidebar"; +import { AppSidebar } from "@multica/ui/components/app-sidebar"; +import { ThemeProvider } from "@multica/ui/components/theme-provider"; +import { Toaster } from "@multica/ui/components/ui/sonner"; +import { HubSidebar } from "./components/hub-sidebar"; -const inter = Inter({subsets:['latin'],variable:'--font-sans'}); +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const geistSans = Geist({ variable: "--font-geist-sans", @@ -14,9 +22,15 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); +const playfair = Playfair_Display({ + variable: "--font-brand", + subsets: ["latin"], + weight: ["400"], +}); + export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Multica", + description: "Distributed AI agent framework", }; export default function RootLayout({ @@ -25,11 +39,26 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} + + + + + + +
{children}
+
+
+
+ ); diff --git a/apps/web/app/lib/config.ts b/apps/web/app/lib/config.ts new file mode 100644 index 00000000..6f4f0c1f --- /dev/null +++ b/apps/web/app/lib/config.ts @@ -0,0 +1,2 @@ +export const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:3000" +export const CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL ?? "http://localhost:4000" diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 5e476424..0aabd23b 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,7 +1,5 @@ -"use client" - -import { ComponentExample } from "@multica/ui/components/component-example"; +import { Chat } from "./components/chat"; export default function Page() { - return ; -} \ No newline at end of file + return ; +} diff --git a/apps/web/package.json b/apps/web/package.json index c9cf44e9..954a4008 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,13 +3,17 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --port 3001", "build": "next build", "start": "next start", "lint": "eslint" }, "dependencies": { + "@multica/sdk": "workspace:*", + "@multica/store": "workspace:*", "@multica/ui": "workspace:*", + "uuid": "^13.0.0", + "zustand": "catalog:", "@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/react": "^1.1.4", "next": "16.1.6", diff --git a/apps/web/public/file.svg b/apps/web/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/apps/web/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/web/public/globe.svg b/apps/web/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/apps/web/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/web/public/icon.png b/apps/web/public/icon.png new file mode 100644 index 00000000..ba5cedcb Binary files /dev/null and b/apps/web/public/icon.png differ diff --git a/apps/web/public/next.svg b/apps/web/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/apps/web/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/web/public/vercel.svg b/apps/web/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/apps/web/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/web/public/window.svg b/apps/web/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/apps/web/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/package.json b/package.json index bf9b5bea..f6dcd5e8 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,13 @@ "multica-profile": "./bin/multica-profile.mjs" }, "scripts": { - "dev": "tsx src/index.ts", - "agent:cli": "tsx src/agent/cli.ts", - "agent:interactive": "tsx src/agent/interactive-cli.ts", - "agent:profile": "tsx src/agent/profile-cli.ts", - "skills:cli": "tsx src/agent/skills-cli.ts", - "dev:gateway": "tsx --watch src/gateway/main.ts", - "dev:console": "tsx --watch src/console/main.ts", + "dev": "concurrently -n gateway,console,web -c blue,yellow,green \"pnpm dev:gateway\" \"pnpm dev:console\" \"pnpm dev:web\"", + "agent:cli": "tsx --env-file=.env src/agent/cli.ts", + "agent:interactive": "tsx --env-file=.env src/agent/interactive-cli.ts", + "agent:profile": "tsx --env-file=.env src/agent/profile-cli.ts", + "skills:cli": "tsx --env-file=.env src/agent/skills-cli.ts", + "dev:gateway": "tsx --env-file=.env --watch src/gateway/main.ts", + "dev:console": "tsx --env-file=.env --watch src/console/main.ts", "dev:web": "pnpm --filter @multica/web dev", "dev:desktop": "pnpm --filter @multica/desktop dev", "build": "turbo build", @@ -38,6 +38,7 @@ "@types/turndown": "^5.0.6", "@types/uuid": "^11.0.0", "@vitest/coverage-v8": "^4.0.18", + "concurrently": "^9.2.1", "esbuild": "^0.27.2", "tsx": "^4.21.0", "turbo": "^2.3.4", diff --git a/packages/store/package.json b/packages/store/package.json index 2e9a547d..2837c0c7 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "exports": { + ".": "./src/index.ts", "./*": "./src/*.ts" }, "dependencies": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 17628c39..49f3133c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,24 +9,34 @@ "./lib/*": "./src/lib/*.ts", "./components/*": "./src/components/*.tsx", "./components/ui/*": "./src/components/ui/*.tsx", + "./components/markdown": "./src/components/markdown/index.ts", + "./components/markdown/*": "./src/components/markdown/*.tsx", "./hooks/*": "./src/hooks/*.ts" }, "dependencies": { - "@multica/store": "workspace:*", "@base-ui/react": "^1.1.0", "@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/react": "^1.1.4", + "@multica/store": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "linkify-it": "^5.0.0", + "next-themes": "^0.4.6", "react": "catalog:", "react-dom": "catalog:", + "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "shadcn": "^3.7.0", + "shiki": "^3.21.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4", "tw-animate-css": "^1.4.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/linkify-it": "^5.0.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", "typescript": "catalog:" diff --git a/packages/ui/src/components/app-sidebar.tsx b/packages/ui/src/components/app-sidebar.tsx new file mode 100644 index 00000000..686e5b3c --- /dev/null +++ b/packages/ui/src/components/app-sidebar.tsx @@ -0,0 +1,40 @@ +import { + Sidebar, + SidebarHeader, + SidebarContent, + SidebarFooter, + SidebarMenu, + SidebarMenuItem, +} from "@multica/ui/components/ui/sidebar" +import { ThemeToggle } from "@multica/ui/components/theme-toggle" + +interface AppSidebarProps { + children?: React.ReactNode +} + +export function AppSidebar({ children }: AppSidebarProps) { + return ( + + +
+ Multica + + Multica + +
+
+ {children} + + + + + + + +
+ ) +} diff --git a/packages/ui/src/components/chat-input.tsx b/packages/ui/src/components/chat-input.tsx new file mode 100644 index 00000000..ce5a3e94 --- /dev/null +++ b/packages/ui/src/components/chat-input.tsx @@ -0,0 +1,55 @@ +"use client"; +import { useRef } from "react"; +import { Button } from "@multica/ui/components/ui/button"; +import { ArrowUpIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { cn } from "@multica/ui/lib/utils"; + +interface ChatInputProps { + onSubmit?: (value: string) => void; + disabled?: boolean; + placeholder?: string; +} + +export function ChatInput({ onSubmit, disabled, placeholder = "Type a message..." }: ChatInputProps) { + const textareaRef = useRef(null); + + const handleSubmit = () => { + const value = textareaRef.current?.value ?? ""; + if (!value.trim()) return; + onSubmit?.(value); + textareaRef.current!.value = ""; + // reset height + textareaRef.current!.style.height = "auto"; + }; + + return ( +
+