From c84911e748c98ae1d0d6ca14b4be8cf311dae1e1 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 4 May 2026 10:08:31 -0700 Subject: [PATCH] feat(canvas-sync): derive voiceCapable + voiceId from agent_config.settings.voice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now `voiceCapable` was a phantom field — defined on the cloud's AgentPresence shape, read by the 🎤 header badge + agents-panel badge, but never produced. The cloud canvas API returned agent shapes with no such field, so every read fell to `?? false` and the badge was dead code. Truth source already exists: `agent_config.settings.voice` (kokoro voice ID, see canvas-interactive.ts hashTts + assignment.ts). Same pattern as identityColor injection in syncCanvas — extend the SQL filter to also match rows with `voice` in settings, then surface `voiceCapable: true` + `voiceId` on the agent shape pushed to cloud. Cloud passes the payload through unchanged (host-canvas-state-routes.ts spreads row.payload), so this single node-side change makes the voiceCapable boolean real end-to-end. Unblocks the speak V0 LocalActionRow lane (kai msg-1777913827161, re-scoped 1777914293792 to fix upstream first). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cloud.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cloud.ts b/src/cloud.ts index 0f893283..3400b971 100644 --- a/src/cloud.ts +++ b/src/cloud.ts @@ -1520,7 +1520,7 @@ async function syncCanvas(): Promise { try { const db = getDb() const settingsRows = db.prepare( - "SELECT agent_id, settings FROM agent_config WHERE settings LIKE '%avatar%' OR settings LIKE '%identityColor%'" + "SELECT agent_id, settings FROM agent_config WHERE settings LIKE '%avatar%' OR settings LIKE '%identityColor%' OR settings LIKE '%voice%'" ).all() as Array<{ agent_id: string; settings: string }> const displayNameByAgent = new Map() for (const role of getAgentRoles()) { @@ -1531,10 +1531,16 @@ async function syncCanvas(): Promise { for (const row of settingsRows) { if (!agents[row.agent_id]) continue try { - const s = JSON.parse(row.settings) as { avatar?: { content?: string }; identityColor?: string } + const s = JSON.parse(row.settings) as { avatar?: { content?: string }; identityColor?: string; voice?: string } const target = agents[row.agent_id] as Record if (s.avatar?.content) target.avatar = s.avatar.content if (typeof s.identityColor === 'string' && s.identityColor) target.identityColor = s.identityColor + // voiceCapable: agent has claimed a kokoro voice (settings.voice non-empty string). + // Read by web AgentPresence.voiceCapable to gate Speak row + 🎤 badge. + if (typeof s.voice === 'string' && s.voice.trim()) { + target.voiceCapable = true + target.voiceId = s.voice.trim() + } } catch { /* skip malformed settings */ } } // Inject displayName for every agent in payload that has a TEAM-ROLES entry,