fix(vision): remote camera connection and session management

- Fix race condition in useRemoteCameraDesktop where session ID wasn't
  saved before socket connection check, preventing auto-reconnect
- Same fix in useRemoteCameraPhone for phone-side connection
- Fix "new session" button in RemoteCameraQRCode - properly clears old
  session and creates new one using prevRef to detect state changes
- Show full QR code UI with copyable URL (removed compact mode)
- Redesign AbacusVisionBridge UI: camera feed as hero, toolbar on feed,
  collapsible crop settings, source selector as tabs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2026-01-01 19:01:33 -06:00
parent 41aa7ff33f
commit 8a454158b5
4 changed files with 596 additions and 464 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@ export interface RemoteCameraQRCodeProps {
size?: number
/** Existing session ID to reuse (for reconnection scenarios) */
existingSessionId?: string | null
/** Compact mode - just the QR code, no instructions or URL */
compact?: boolean
}
/**
@@ -28,14 +30,18 @@ export function RemoteCameraQRCode({
onSessionCreated,
size = 200,
existingSessionId,
compact = false,
}: RemoteCameraQRCodeProps) {
const { session, isCreating, error, createSession, setExistingSession, getPhoneUrl } =
const { session, isCreating, error, createSession, setExistingSession, clearSession, getPhoneUrl } =
useRemoteCameraSession()
// Ref to track if we've already initiated session creation
// This prevents React 18 Strict Mode from creating duplicate sessions
const creationInitiatedRef = useRef(false)
// Track previous existingSessionId to detect when it changes TO null
const prevExistingSessionIdRef = useRef<string | null | undefined>(existingSessionId)
// If we have an existing session ID, use it instead of creating a new one
useEffect(() => {
if (existingSessionId && !session) {
@@ -43,6 +49,19 @@ export function RemoteCameraQRCode({
}
}, [existingSessionId, session, setExistingSession])
// Reset when existingSessionId CHANGES from truthy to null (user wants fresh session)
// This prevents clearing sessions that we just created ourselves
useEffect(() => {
const prevId = prevExistingSessionIdRef.current
prevExistingSessionIdRef.current = existingSessionId
// Only clear if existingSessionId changed FROM something TO null
if (prevId && !existingSessionId) {
clearSession()
creationInitiatedRef.current = false
}
}, [existingSessionId, clearSession])
// Create session on mount only if no existing session
// Use ref to prevent duplicate creation in React 18 Strict Mode
useEffect(() => {
@@ -143,6 +162,22 @@ export function RemoteCameraQRCode({
return null
}
// Compact mode - just the QR code in a minimal container
if (compact) {
return (
<div
className={css({
bg: 'white',
p: 2,
borderRadius: 'lg',
})}
data-component="remote-camera-qr-compact"
>
<AbacusQRCode value={phoneUrl} size={size} />
</div>
)
}
return (
<div
className={css({

View File

@@ -254,16 +254,19 @@ export function useRemoteCameraDesktop(): UseRemoteCameraDesktopReturn {
'connected:',
isConnected
)
if (!socket || !isConnected) {
console.error('[RemoteCameraDesktop] Socket not connected!')
setError('Socket not connected')
return
}
// Save session ID FIRST, so auto-connect handler can use it
// even if socket isn't connected yet
currentSessionIdRef.current = sessionId
setCurrentSessionId(sessionId)
persistSessionId(sessionId)
setError(null)
if (!socket || !isConnected) {
console.log('[RemoteCameraDesktop] Socket not connected yet, will subscribe on connect')
return
}
console.log('[RemoteCameraDesktop] Emitting remote-camera:subscribe')
socket.emit('remote-camera:subscribe', { sessionId })
},

View File

@@ -354,15 +354,17 @@ export function useRemoteCameraPhone(
'connected:',
isSocketConnected
)
if (!socket || !isSocketConnected) {
console.error('[RemoteCameraPhone] Socket not connected!')
setError('Socket not connected')
return
}
// Save session ID FIRST, so auto-connect handler can use it
// even if socket isn't connected yet
sessionIdRef.current = sessionId
setError(null)
if (!socket || !isSocketConnected) {
console.log('[RemoteCameraPhone] Socket not connected yet, will join on connect')
return
}
console.log('[RemoteCameraPhone] Emitting remote-camera:join')
socket.emit('remote-camera:join', { sessionId })
setIsConnected(true)