multica/apps/web
Naiyuan Qing a088875118 refactor(ui): move theme toggle from Chat to web app layout
Chat component no longer depends on next-themes, making it safe to use
in the desktop app. Theme toggle is now a fixed button in the web layout.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:02:48 +08:00
..
app refactor(ui): move theme toggle from Chat to web app layout 2026-02-03 20:02:48 +08:00
public feat(web): add PWA support for installability and offline fallback 2026-02-03 14:43:58 +08:00
.gitignore Add monorepo setup with Turborepo and Web client 2026-01-29 16:33:35 +08:00
components.json refactor(ui): reorganize components into ui/ subdirectory and share layout 2026-01-30 11:34:09 +08:00
eslint.config.mjs Add monorepo setup with Turborepo and Web client 2026-01-29 16:33:35 +08:00
next.config.ts feat(web): add PWA support for installability and offline fallback 2026-02-03 14:43:58 +08:00
package.json refactor(ui): move theme toggle from Chat to web app layout 2026-02-03 20:02:48 +08:00
postcss.config.mjs refactor(web): update imports to use @multica/ui package 2026-01-30 11:34:05 +08:00
README.md docs(web): add development guidelines README 2026-02-02 11:52:47 +08:00
tsconfig.json feat(store): add shared Zustand store package with counter example 2026-01-30 11:34:09 +08:00

@multica/web

Next.js web client for Super Multica. This app is a thin shell — it contains only layout and page entry points. All business logic, state management, UI components, and network requests live in shared packages.

Architecture

apps/web/app/
├── layout.tsx   — Root layout, setConfig(), providers
├── page.tsx     — Page entry, renders <Chat />
└── icon.png     — Favicon

Everything else comes from packages:

Package Responsibility Examples
@multica/store Global state (Zustand) Hub, Messages, Gateway, DeviceId
@multica/ui Components & UI hooks Chat, HubSidebar, Skeleton, useScrollFade
@multica/fetch HTTP client & URL config consoleApi, setConfig()
@multica/sdk WebSocket client GatewayClient

Where does new code go?

  • Page-scoped UI hook (e.g. form toggle, scroll position) → @multica/ui/hooks/
  • Cross-component state (e.g. user preferences, notifications) → @multica/store
  • Reusable component@multica/ui/components/
  • HTTP request helper@multica/fetch
  • This app → Only if it's Next.js-specific (middleware, route handlers, next.config)

Principle: desktop also consumes these packages, so anything reusable must NOT live in apps/web.

Network Requests

Two communication channels, two packages:

HTTP  → @multica/fetch (consoleApi)  → Console :4000  (Hub, Agent CRUD)
WS    → @multica/store (gateway)     → Gateway :3000  (Chat messages)

Rules:

  1. Never hardcode URLs. Use consoleApi for HTTP, useGatewayStore for WS. Both read from setConfig() in layout.tsx.
  2. HTTP for management, WS for real-time. Creating/deleting agents is HTTP. Sending/receiving chat messages is WS.
  3. Future: gateway may proxy HTTP. The current two-endpoint setup may merge into one. Because all requests go through @multica/fetch and @multica/store, business code won't need changes.

State Management

We use Zustand. Follow these rules:

Subscribe only to what you render

// Good — component re-renders only when status changes
const status = useHubStore((s) => s.status)

// Bad — re-renders on ANY store change
const { status } = useHubStore()

Use getState() in callbacks

Don't subscribe to state that's only used inside event handlers. Read it at call time instead.

// Good — no subscription, no re-render
const handleSend = useCallback((text: string) => {
  const hub = useHubStore.getState().hub
  const agentId = useHubStore.getState().activeAgentId
  if (!hub?.hubId || !agentId) return
  useMessagesStore.getState().addUserMessage(text, agentId)
  useGatewayStore.getState().send(hub.hubId, "message", { agentId, content: text })
}, [])

// Bad — subscribes to hub and activeAgentId just to use them in onClick
const hub = useHubStore((s) => s.hub)
const activeAgentId = useHubStore((s) => s.activeAgentId)

Subscribe to derived values, not raw objects

// Good — re-renders only when the boolean flips
const isConnected = useHubStore((s) => s.status === "connected")

// Bad — re-renders when any field of hub changes
const hub = useHubStore((s) => s.hub)
const isConnected = hub !== null

Filter/derive with useMemo, not inside selectors

Selectors that return new references (.filter(), .map()) cause infinite re-renders. Derive outside the selector.

// Good
const messages = useMessagesStore((s) => s.messages)
const filtered = useMemo(
  () => messages.filter((m) => m.agentId === activeAgentId),
  [messages, activeAgentId]
)

// Bad — .filter() returns a new array every time, triggers infinite loop
const filtered = useMessagesStore((s) => s.messages.filter(...))

Initialize once

Side-effectful operations (WS connection, SDK init) must have guards to prevent double execution.

// Inside store
connect: (deviceId) => {
  if (client) return  // Already connected, skip
  client = new GatewayClient(...)
  client.connect()
}

Imports

Use direct paths for @multica/ui

// Good
import { Chat } from "@multica/ui/components/chat"
import { Button } from "@multica/ui/components/ui/button"
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade"

// Bad — barrel import pulls in everything
import { Chat, Button, useScrollFade } from "@multica/ui"

@multica/store barrel import is fine — it has few exports and all are lightweight Zustand stores.

Heavy components: use dynamic import

For large dependencies (code editors, chart libraries, PDF viewers), lazy-load to keep the initial bundle small.

import dynamic from "next/dynamic"

const CodeEditor = dynamic(
  () => import("@multica/ui/components/code-editor"),
  { ssr: false }
)

Conditional Rendering

Use ternary expressions, not &&, to avoid rendering 0 or "" as visible content.

// Good
{status === "connected" ? <AgentList /> : null}

// Bad — if agents is 0, renders "0" on screen
{agents.length && <AgentList />}

Development

# Start web dev server (port 3001)
multica dev web

# Or start all services
multica dev

# Typecheck
cd apps/web && npx tsc --noEmit

Adding a New Feature — Checklist

  1. Does it need global state? → Create a store in @multica/store
  2. Does it need HTTP calls? → Use consoleApi from @multica/fetch
  3. Does it need a UI component? → Add to @multica/ui/components/
  4. Does it need a UI hook? → Add to @multica/ui/hooks/
  5. Is it Next.js-specific? → Only then add to apps/web
  6. Is the component heavy (>50KB)? → Use next/dynamic with { ssr: false }