+ {isConnected && (
+
+
+
+ )}
+
- {!isConnected ? (
-
-
-
Paste a connection code to start
- {(gwState === "connecting" || gwState === "connected") && (
-
Connecting...
- )}
-
-
-
-
- ) : filtered.length === 0 ? (
+ {loading ? (
+
+ ) : !isConnected ? (
+
+ ) : messages.length === 0 ? (
Send a message to start the conversation
) : (
-
- {filtered.map((msg) => {
- const isStreaming = streamingIds.has(msg.id)
- return (
-
-
- {isStreaming ? (
-
- ) : (
-
- {msg.content}
-
- )}
-
-
- )
- })}
-
+
)}
diff --git a/packages/ui/src/components/connect-prompt.tsx b/packages/ui/src/components/connect-prompt.tsx
new file mode 100644
index 00000000..8d3fbb0b
--- /dev/null
+++ b/packages/ui/src/components/connect-prompt.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { useState, useCallback } from "react";
+import { Button } from "@multica/ui/components/ui/button";
+import { Textarea } from "@multica/ui/components/ui/textarea";
+import { toast } from "@multica/ui/components/ui/sonner";
+import {
+ useConnectionStore,
+ parseConnectionCode,
+ saveConnection,
+} from "@multica/store";
+
+export function ConnectPrompt() {
+ const gwState = useConnectionStore((s) => s.connectionState)
+ const [codeInput, setCodeInput] = useState("")
+
+ const handleConnect = useCallback(() => {
+ const trimmed = codeInput.trim()
+ if (!trimmed) return
+ try {
+ const info = parseConnectionCode(trimmed)
+ saveConnection(info)
+ useConnectionStore.getState().connect(info)
+ setCodeInput("")
+ } catch (e) {
+ toast.error((e as Error).message)
+ }
+ }, [codeInput])
+
+ return (
+
+
+
Paste a connection code to start
+ {(gwState === "connecting" || gwState === "connected") && (
+
Connecting...
+ )}
+
+
+
+
+ )
+}
diff --git a/packages/ui/src/components/hub-sidebar.tsx b/packages/ui/src/components/hub-sidebar.tsx
deleted file mode 100644
index a64b232e..00000000
--- a/packages/ui/src/components/hub-sidebar.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import {
- SidebarGroup,
- SidebarGroupLabel,
- SidebarGroupAction,
- SidebarGroupContent,
- SidebarMenu,
- SidebarMenuItem,
-} from "@multica/ui/components/ui/sidebar"
-import { Button } from "@multica/ui/components/ui/button"
-import { Input } from "@multica/ui/components/ui/input"
-import { HugeiconsIcon } from "@hugeicons/react"
-import { PlusSignIcon, Delete02Icon } from "@hugeicons/core-free-icons"
-import { useHubStore, useDeviceId, useGatewayStore } from "@multica/store"
-import { useHubInit } from "@multica/store"
-import { Skeleton } from "@multica/ui/components/ui/skeleton"
-
-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 createAgent = useHubStore((s) => s.createAgent)
- const deleteAgent = useHubStore((s) => s.deleteAgent)
- const setActiveAgentId = useHubStore((s) => s.setActiveAgentId)
-
- const gwState = useGatewayStore((s) => s.connectionState)
- const hubId = useGatewayStore((s) => s.hubId)
- const hubs = useGatewayStore((s) => s.hubs)
- const setHubId = useGatewayStore((s) => s.setHubId)
- const listDevices = useGatewayStore((s) => s.listDevices)
-
- const [hubIdInput, setHubIdInput] = useState("")
- const isRegistered = gwState === "registered"
- const needsHubSelection = isRegistered && !hubId
-
- const handleConnect = () => {
- const id = hubIdInput.trim()
- if (!id) return
- setHubId(id)
- }
-
- const handleDisconnect = () => {
- useGatewayStore.setState({ hubId: null })
- useHubStore.setState({ status: "idle", hub: null, agents: [], activeAgentId: null })
- }
-
- return (
- <>
-
- Hub
-
- {!isRegistered ? (
-
-
-
- {gwState === "disconnected" ? "Connecting to Gateway..." : "Registering..."}
-
-
- ) : needsHubSelection ? (
-
- {hubs.length > 0 && (
-
- {hubs.map((h) => (
-
- ))}
-
- )}
-
setHubIdInput(e.target.value)}
- placeholder="Or enter Hub ID..."
- className="h-7 text-xs font-mono"
- onKeyDown={(e) => e.key === "Enter" && handleConnect()}
- />
-
-
-
-
-
- ) : (
- <>
-
-
-
- {STATUS_LABEL[status]}
-
-
- {status === "connected" && hub ? (
-
- {hub.hubId}
-
- ) : (status === "idle" || status === "loading") ? (
-
- ) : null}
-
-
-
- >
- )}
-
-
-
- {isRegistered && hubId && (status === "idle" || status === "loading") && (
-
- Agents
-
-
-
-
-
-
-
-
- )}
-
- {status === "connected" && (
-
- Agents
- createAgent()} title="Create agent">
-
-
-
-
- {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/packages/ui/src/components/markdown/StreamingMarkdown.tsx b/packages/ui/src/components/markdown/StreamingMarkdown.tsx
index 65b585a8..1bb30d4f 100644
--- a/packages/ui/src/components/markdown/StreamingMarkdown.tsx
+++ b/packages/ui/src/components/markdown/StreamingMarkdown.tsx
@@ -16,8 +16,16 @@ interface Block {
}
/**
- * Simple hash function for cache keys
- * Uses djb2 algorithm - fast and produces good distribution
+ * djb2 hash (XOR variant) by Daniel J. Bernstein.
+ * Used to generate stable React keys for completed content blocks.
+ *
+ * - 5381: empirically chosen initial value that produces fewer collisions
+ * - (hash << 5) + hash: equivalent to hash * 33
+ * - ^ charCode: XOR variant, favored by Bernstein over additive version
+ * - >>> 0: convert to unsigned 32-bit integer
+ *
+ * Not cryptographic — just fast with good distribution for short strings.
+ * @see http://www.cse.yorku.ca/~oz/hash.html
*/
function simpleHash(str: string): string {
let hash = 5381
@@ -164,7 +172,7 @@ export function StreamingMarkdown({
}
const indicator = (
-
+
Generating...
diff --git a/packages/ui/src/components/message-list.tsx b/packages/ui/src/components/message-list.tsx
new file mode 100644
index 00000000..937230e6
--- /dev/null
+++ b/packages/ui/src/components/message-list.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { MemoizedMarkdown } from "@multica/ui/components/markdown";
+import { StreamingMarkdown } from "@multica/ui/components/markdown/StreamingMarkdown";
+import { cn } from "@multica/ui/lib/utils";
+import type { Message } from "@multica/store";
+
+interface MessageListProps {
+ messages: Message[]
+ streamingIds: Set
+}
+
+export function MessageList({ messages, streamingIds }: MessageListProps) {
+ return (
+
+ {messages.map((msg) => {
+ const isStreaming = streamingIds.has(msg.id)
+ return (
+
+
+ {isStreaming ? (
+
+ ) : (
+
+ {msg.content}
+
+ )}
+
+
+ )
+ })}
+
+ )
+}