multica/apps/web
Naiyuan Qing 178d71524f feat(web): add useGatewayConnection and useChat hooks
Replace Zustand global stores with hook-local state for the web app.
useGatewayConnection handles client lifecycle, identity persistence,
reconnection, and keyed reset. useChat handles message history, streaming
events, tool execution, Hub error action, and exec approval requests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 14:43:53 +08:00
..
app chore(web): replace old icons with company logo 2026-02-04 10:20:02 +08:00
hooks feat(web): add useGatewayConnection and useChat hooks 2026-02-05 14:43:53 +08:00
public chore(web): update PWA logo assets 2026-02-05 09:23:41 +08:00
.gitignore chore(web): gitignore self-signed HTTPS certificates 2026-02-04 15:41:17 +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 chore(web): enable experimental HTTPS and fix scanner active states 2026-02-05 10:44:29 +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 }