From 037908cf8da9ff860fb8e34c175185cc23aab73f Mon Sep 17 00:00:00 2001
From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Date: Thu, 5 Feb 2026 10:44:15 +0800
Subject: [PATCH 1/2] feat(connection): add device verification status feedback
for collaborators
Add a "verifying" connection state between "connected" and "registered"
so collaborators see clear feedback while waiting for the device owner
to approve their connection on Desktop.
Changes across the stack:
- Hub: verify RPC returns isNewDevice flag to distinguish new vs whitelisted
- SDK: emit "verifying" state before verify RPC, pass isNewDevice through
- Store: capture isNewDevice via onVerified, capture rejection via onError
- UI: ConnectionStatus (waiting), RejectedStatus (declined), and
verify success overlay (approved) replace the stuck scanner screen
Co-Authored-By: Claude Opus 4.5
---
packages/sdk/src/client.ts | 5 +-
packages/sdk/src/types.ts | 3 +-
packages/store/src/connection-store.ts | 6 +
packages/ui/src/components/chat.tsx | 40 ++++++-
packages/ui/src/components/connect-prompt.tsx | 104 ++++++++++++++++--
src/hub/rpc/handlers/verify.ts | 4 +-
6 files changed, 144 insertions(+), 18 deletions(-)
diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts
index fb362281..cf7bf932 100644
--- a/packages/sdk/src/client.ts
+++ b/packages/sdk/src/client.ts
@@ -234,7 +234,7 @@ export class GatewayClient {
}
/** Hub 验证成功回调 */
- onVerified(callback: (result: { hubId: string; agentId: string }) => void): this {
+ onVerified(callback: (result: { hubId: string; agentId: string; isNewDevice?: boolean }) => void): this {
this.callbacks.onVerified = callback;
return this;
}
@@ -312,12 +312,13 @@ export class GatewayClient {
if (this.options.hubId) {
// Set internal state to allow send/request during verify
this._state = "registered";
+ this.callbacks.onStateChange?.("verifying");
const meta = typeof navigator !== "undefined" ? {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
} : undefined;
- this.request<{ hubId: string; agentId: string }>(
+ this.request<{ hubId: string; agentId: string; isNewDevice?: boolean }>(
this.options.hubId,
"verify",
{ token: this.options.token, meta },
diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts
index 88118021..f8a3f693 100644
--- a/packages/sdk/src/types.ts
+++ b/packages/sdk/src/types.ts
@@ -102,6 +102,7 @@ export type ConnectionState =
| "disconnected"
| "connecting"
| "connected"
+ | "verifying"
| "registered";
/** Event callback types */
@@ -109,7 +110,7 @@ export interface GatewayClientCallbacks {
onConnect?: (socketId: string) => void;
onDisconnect?: (reason: string) => void;
onRegistered?: (deviceId: string) => void;
- onVerified?: (result: { hubId: string; agentId: string }) => void;
+ onVerified?: (result: { hubId: string; agentId: string; isNewDevice?: boolean }) => void;
onMessage?: (message: RoutedMessage) => void;
onSendError?: (error: SendErrorResponse) => void;
onPong?: (data: string) => void;
diff --git a/packages/store/src/connection-store.ts b/packages/store/src/connection-store.ts
index fd5c09e0..17b41a51 100644
--- a/packages/store/src/connection-store.ts
+++ b/packages/store/src/connection-store.ts
@@ -35,6 +35,8 @@ interface ConnectionStoreState {
agentId: string | null
connectionState: ConnectionState
lastError: { code: string; message: string } | null
+ /** Whether the current connection required Owner approval (new device) */
+ isNewDevice: boolean | null
}
interface ConnectionStoreActions {
@@ -158,6 +160,8 @@ function createClient(
useMessagesStore.getState().addAssistantMessage(payload.content, payload.agentId)
}
})
+ .onVerified((result) => set({ isNewDevice: result.isNewDevice ?? false }))
+ .onError((error) => set({ lastError: { code: "VERIFY_ERROR", message: error.message } }))
.onSendError((error) => set({ lastError: { code: error.code, message: error.error } }))
}
@@ -252,6 +256,7 @@ export const useConnectionStore = create()(
agentId: null,
connectionState: "disconnected",
lastError: null,
+ isNewDevice: null,
// Connect using a connection code (disconnect existing connection first)
connect: (code) => {
@@ -284,6 +289,7 @@ export const useConnectionStore = create()(
hubId: null,
agentId: null,
lastError: null,
+ isNewDevice: null,
})
},
diff --git a/packages/ui/src/components/chat.tsx b/packages/ui/src/components/chat.tsx
index ea51d036..e862eac8 100644
--- a/packages/ui/src/components/chat.tsx
+++ b/packages/ui/src/components/chat.tsx
@@ -1,11 +1,14 @@
"use client";
-import { useRef, useCallback } from "react";
+import { useRef, useCallback, useState, useEffect } from "react";
import { Button } from "@multica/ui/components/ui/button";
import { ChatInput } from "@multica/ui/components/chat-input";
import { useConnectionStore, useMessagesStore, useAutoConnect } from "@multica/store";
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
import { useAutoScroll } from "@multica/ui/hooks/use-auto-scroll";
+import { useIsMobile } from "@multica/ui/hooks/use-mobile";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { CheckmarkCircle02Icon } from "@hugeicons/core-free-icons";
import { ConnectPrompt } from "./connect-prompt";
import { MessageList } from "./message-list";
import { ChatSkeleton } from "./chat-skeleton";
@@ -17,10 +20,25 @@ export function Chat() {
const gwState = useConnectionStore((s) => s.connectionState)
const hubId = useConnectionStore((s) => s.hubId)
const lastError = useConnectionStore((s) => s.lastError)
+ const isNewDevice = useConnectionStore((s) => s.isNewDevice)
+ const isMobile = useIsMobile()
const messages = useMessagesStore((s) => s.messages)
const streamingIds = useMessagesStore((s) => s.streamingIds)
+ // Show success overlay for 2s when a new device is approved by Owner
+ const [showVerifySuccess, setShowVerifySuccess] = useState(false)
+ useEffect(() => {
+ if (gwState === "registered" && isNewDevice === true) {
+ setShowVerifySuccess(true)
+ const timer = setTimeout(() => {
+ setShowVerifySuccess(false)
+ useConnectionStore.setState({ isNewDevice: null })
+ }, 2000)
+ return () => clearTimeout(timer)
+ }
+ }, [gwState, isNewDevice])
+
const isConnected = gwState === "registered" && !!hubId && !!agentId
const handleSend = useCallback((text: string) => {
@@ -39,6 +57,26 @@ export function Chat() {
return (
+ {/* Verify success overlay — shown for 2s when new device approved */}
+ {showVerifySuccess && (
+
+
+
+
Connected
+
+ Your device has been approved
+
+
+
+ )}
+
{isConnected && (
@@ -109,11 +194,6 @@ export function ConnectPrompt() {
? "Scan a Multica QR code to start chatting"
: "Paste a Multica connection code to start chatting"}