From 2a80b7751b059f45c39c09d3037cb4f61dddc83c Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 14 Feb 2026 23:30:56 -0600 Subject: [PATCH] feat: Add torch client identity, early connection and auto-request - Named clients persisted in localStorage, editable from dropdown - Auto-request: clients can auto-receive torch when no holder exists - Early torch init in App.vue (fires before WebMCP, awaited later) - Deferred torch connection in toolRegistry for race condition safety - TorchButton rewritten as dropdown with name editor, toggle, client list - Server broadcasts client names, supports update-name message - MCP tool handlers display client names --- frontend/src/App.vue | 7 +- frontend/src/components/TorchButton.vue | 631 ++++++++++++++---- frontend/src/services/toolRegistry.ts | 10 +- .../services/tools/handlers/torchHandlers.ts | 10 +- frontend/src/services/torch.ts | 25 +- frontend/src/stores/torch.ts | 18 + server/services/handlers/torch-handler.ts | 29 +- server/services/sync-server.ts | 2 +- 8 files changed, 597 insertions(+), 135 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c892249..03c2808 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -244,6 +244,9 @@ onMounted(async () => { // Connect to WebSocket for Claude status updates connectStatusWs() + // Fire torch connection early (don't await yet) + const torchReady = initTorch() + // Initialize WebMCP connection await initWebMCP() @@ -320,8 +323,8 @@ onMounted(async () => { }) } - // Initialize torch system (handles MCP connection based on torch state) - await initTorch() + // Ensure torch connection is established + await torchReady }) onUnmounted(() => { diff --git a/frontend/src/components/TorchButton.vue b/frontend/src/components/TorchButton.vue index 3055e0e..5d41d56 100644 --- a/frontend/src/components/TorchButton.vue +++ b/frontend/src/components/TorchButton.vue @@ -1,23 +1,26 @@ diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 5e707c8..dcec961 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -170,6 +170,7 @@ const pageCategories: Record = { let currentPage: PageName | null = null let isInitialized = false +let pendingTorchConnection = false /** * Check if connected to MCP @@ -185,6 +186,12 @@ function isConnectedToMCP(): boolean { export function initToolRegistry(router: any) { setRouter(router) isInitialized = true + + // Fulfill any pending torch connection that arrived before init + if (pendingTorchConnection) { + pendingTorchConnection = false + onTorchConnected() + } } /** @@ -291,7 +298,8 @@ export async function initToolsOnRefresh(pageName: PageName) { */ export async function onTorchConnected() { if (!isInitialized) { - console.warn('[ToolRegistry] Not initialized') + console.log('[ToolRegistry] Not initialized yet, deferring torch connection') + pendingTorchConnection = true return } diff --git a/frontend/src/services/tools/handlers/torchHandlers.ts b/frontend/src/services/tools/handlers/torchHandlers.ts index 8d57142..2b7600a 100644 --- a/frontend/src/services/tools/handlers/torchHandlers.ts +++ b/frontend/src/services/tools/handlers/torchHandlers.ts @@ -25,7 +25,7 @@ export function createTorchHandlers(): ToolConfig[] { const isMe = client.id === status.clientId const hasTorch = client.hasTorch - result += `${hasTorch ? '🔥' : '⚪'} ${client.id}${isMe ? ' (este browser)' : ''}\n` + result += `${hasTorch ? '🔥' : '⚪'} ${client.name || 'Anonymous'} (${client.id})${isMe ? ' (este browser)' : ''}\n` result += ` Hostname: ${client.hostname}\n` result += ` User Agent: ${client.userAgent.substring(0, 80)}...\n` result += ` Conectado: ${client.connectedAt}\n\n` @@ -46,8 +46,10 @@ export function createTorchHandlers(): ToolConfig[] { const status = getTorchStatus() const clients = getTorchClients() + const myClient = clients.find(c => c.id === status.clientId) let result = '=== Estado de la Antorcha ===\n\n' result += `Mi ID: ${status.clientId || 'No registrado'}\n` + result += `Mi nombre: ${myClient?.name || 'Anonymous'}\n` result += `Tengo la antorcha: ${status.hasTorch ? 'Si' : 'No'}\n` result += `Holder actual: ${status.torchHolderId || 'Nadie'}\n` result += `Clientes conectados: ${status.clientCount}\n\n` @@ -56,6 +58,7 @@ export function createTorchHandlers(): ToolConfig[] { const holder = clients.find(c => c.id === status.torchHolderId) if (holder) { result += `Detalles del holder:\n` + result += ` Nombre: ${holder.name || 'Anonymous'}\n` result += ` Hostname: ${holder.hostname}\n` result += ` Conectado desde: ${holder.connectedAt}\n` } @@ -89,16 +92,17 @@ export function createTorchHandlers(): ToolConfig[] { const targetExists = clients.some(c => c.id === args.target_id) if (!targetExists) { - return `Error: Cliente "${args.target_id}" no encontrado.\n\nClientes disponibles:\n${clients.map(c => ` - ${c.id}`).join('\n')}` + return `Error: Cliente "${args.target_id}" no encontrado.\n\nClientes disponibles:\n${clients.map(c => ` - ${c.name || 'Anonymous'} (${c.id})`).join('\n')}` } if (args.target_id === status.clientId) { return 'Error: No puedes transferir la antorcha a ti mismo' } + const targetClient = clients.find(c => c.id === args.target_id) const success = await transferTorch(args.target_id) if (success) { - return `Antorcha transferida a ${args.target_id}. El otro browser ahora tiene control del MCP.` + return `Antorcha transferida a ${targetClient?.name || args.target_id}. El otro browser ahora tiene control del MCP.` } else { return 'Error al transferir la antorcha' } diff --git a/frontend/src/services/torch.ts b/frontend/src/services/torch.ts index fc03742..8b20f5e 100644 --- a/frontend/src/services/torch.ts +++ b/frontend/src/services/torch.ts @@ -22,11 +22,14 @@ function connectToTorchServer(): Promise { torchWs.onopen = () => { console.log('[Torch] Connected to server') - // Register this client + const torchStore = useTorchStore() + // Register this client with name and autoRequest torchWs?.send(JSON.stringify({ type: 'register', userAgent: navigator.userAgent, - hostname: window.location.hostname + hostname: window.location.hostname, + name: torchStore.clientName || 'Anonymous', + autoRequest: torchStore.autoRequest })) resolve() } @@ -93,6 +96,12 @@ async function handleMessage(data: any) { console.log('[Torch] Got torch, connecting to MCP') await connectToMCP() } + + // Auto-request: if no one holds the torch and we have autoRequest enabled + if (!hasTorchNow && data.holderId === null && torchStore.autoRequest) { + console.log('[Torch] Auto-requesting torch (no holder)') + requestTorch() + } break } @@ -157,6 +166,18 @@ export async function transferTorch(targetId: string): Promise { return true } +/** + * Update client name on server + */ +export function updateName(name: string): void { + const torchStore = useTorchStore() + torchStore.setClientName(name) + + if (torchWs?.readyState === WebSocket.OPEN) { + torchWs.send(JSON.stringify({ type: 'update-name', name })) + } +} + /** * Get list of connected clients */ diff --git a/frontend/src/stores/torch.ts b/frontend/src/stores/torch.ts index e8d46da..84cc529 100644 --- a/frontend/src/stores/torch.ts +++ b/frontend/src/stores/torch.ts @@ -3,6 +3,7 @@ import { ref, computed } from 'vue' export interface TorchClient { id: string + name: string userAgent: string hostname: string url: string @@ -17,6 +18,8 @@ export const useTorchStore = defineStore('torch', () => { const torchHolderId = ref(null) const clients = ref([]) const isRequesting = ref(false) + const clientName = ref(localStorage.getItem('torch-client-name') || '') + const autoRequest = ref(localStorage.getItem('torch-auto-request') === 'true') // Computed const isConnected = computed(() => clientId.value !== null) @@ -44,12 +47,23 @@ export const useTorchStore = defineStore('torch', () => { isRequesting.value = value } + function setClientName(name: string) { + clientName.value = name + localStorage.setItem('torch-client-name', name) + } + + function setAutoRequest(val: boolean) { + autoRequest.value = val + localStorage.setItem('torch-auto-request', String(val)) + } + function reset() { clientId.value = null hasTorch.value = false torchHolderId.value = null clients.value = [] isRequesting.value = false + // Do NOT reset clientName/autoRequest — persist across sessions } return { @@ -59,6 +73,8 @@ export const useTorchStore = defineStore('torch', () => { torchHolderId, clients, isRequesting, + clientName, + autoRequest, // Computed isConnected, torchHolder, @@ -67,6 +83,8 @@ export const useTorchStore = defineStore('torch', () => { setTorchState, setClients, setRequesting, + setClientName, + setAutoRequest, reset } }) diff --git a/server/services/handlers/torch-handler.ts b/server/services/handlers/torch-handler.ts index 3ce7924..5d7d804 100644 --- a/server/services/handlers/torch-handler.ts +++ b/server/services/handlers/torch-handler.ts @@ -8,6 +8,7 @@ interface TorchClient { ws: any id: string + name: string userAgent: string hostname: string connectedAt: Date @@ -27,6 +28,7 @@ function generateClientId(): string { function broadcastTorchState(broadcast: (message: string, filter?: (ws: any) => boolean) => void) { const clientList = Array.from(torchClients.values()).map(c => ({ id: c.id, + name: c.name, userAgent: c.userAgent, hostname: c.hostname, connectedAt: c.connectedAt.toISOString(), @@ -50,6 +52,7 @@ export function handleTorchConnect(ws: any, broadcast: (message: string, filter? torchClients.set(ws, { ws, id, + name: 'Anonymous', userAgent: 'Unknown', hostname: 'Unknown', connectedAt: new Date() @@ -68,8 +71,14 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri case 'register': { client.userAgent = data.userAgent || 'Unknown' client.hostname = data.hostname || 'Unknown' + client.name = data.name || 'Anonymous' + + // Auto-grant torch if requested and no one holds it + if (data.autoRequest && torchHolderId === null) { + torchHolderId = client.id + console.log(`[Torch] Auto-granted torch to ${client.name} (${client.id})`) + } - // No auto-assign - torch must be explicitly requested const hasTorch = torchHolderId === client.id ws.send(JSON.stringify({ @@ -78,7 +87,15 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri hasTorch })) - console.log(`[Torch] Registered: ${client.id} (torch: ${hasTorch})`) + console.log(`[Torch] Registered: ${client.name} (${client.id}) (torch: ${hasTorch})`) + broadcastTorchState(broadcast) + break + } + + case 'update-name': { + const newName = (data.name || '').substring(0, 20) || 'Anonymous' + client.name = newName + console.log(`[Torch] Name updated: ${client.id} → ${newName}`) broadcastTorchState(broadcast) break } @@ -88,7 +105,7 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri torchHolderId = client.id ws.send(JSON.stringify({ type: 'granted' })) - console.log(`[Torch] Transferred: ${previousHolder} → ${client.id}`) + console.log(`[Torch] Transferred: ${previousHolder} → ${client.name} (${client.id})`) broadcastTorchState(broadcast) break } @@ -97,7 +114,7 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri if (torchHolderId === client.id) { torchHolderId = null ws.send(JSON.stringify({ type: 'released' })) - console.log(`[Torch] Released by: ${client.id}`) + console.log(`[Torch] Released by: ${client.name} (${client.id})`) broadcastTorchState(broadcast) } break @@ -135,7 +152,7 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri torchHolderId = targetId ws.send(JSON.stringify({ type: 'transferred', targetId })) - console.log(`[Torch] Transferred by ${client.id}: ${previousHolder} → ${targetId}`) + console.log(`[Torch] Transferred by ${client.name} (${client.id}): ${previousHolder} → ${targetId}`) broadcastTorchState(broadcast) break } @@ -148,7 +165,7 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri export function handleTorchDisconnect(ws: any, broadcast: (message: string, filter?: (ws: any) => boolean) => void) { const client = torchClients.get(ws) if (client) { - console.log(`[Torch] Client disconnected: ${client.id}`) + console.log(`[Torch] Client disconnected: ${client.name} (${client.id})`) // If this client had the torch, release it (no auto-assign) if (torchHolderId === client.id) { diff --git a/server/services/sync-server.ts b/server/services/sync-server.ts index 8815534..5080106 100644 --- a/server/services/sync-server.ts +++ b/server/services/sync-server.ts @@ -78,7 +78,7 @@ export function startSyncServer() { const data = JSON.parse(message.toString()) // Route to appropriate handler based on message type - if (data.type?.startsWith('torch-') || ['register', 'request', 'release', 'transfer'].includes(data.type)) { + if (data.type?.startsWith('torch-') || ['register', 'request', 'release', 'transfer', 'update-name'].includes(data.type)) { handleTorchMessage(ws, data, broadcast) } // Git doesn't expect messages from client