apteva 0.4.53 → 0.4.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/ActivityPage.kxzzb4yc.js +3 -0
  2. package/dist/ApiDocsPage.zq998hbm.js +4 -0
  3. package/dist/App.55rea8mn.js +61 -0
  4. package/dist/App.5ywb23z4.js +53 -0
  5. package/dist/App.6thds120.js +4 -0
  6. package/dist/{App.jhb45d7r.js → App.9tctxzqm.js} +3 -3
  7. package/dist/App.a8r8ttaz.js +4 -0
  8. package/dist/App.agsv5bje.js +4 -0
  9. package/dist/App.cepapqmx.js +4 -0
  10. package/dist/App.dp041gb3.js +221 -0
  11. package/dist/App.fds72zb5.js +4 -0
  12. package/dist/App.fg9qj2dq.js +4 -0
  13. package/dist/App.ndfejbm9.js +4 -0
  14. package/dist/App.nxmfmq1h.js +13 -0
  15. package/dist/App.qdfyt8ba.js +4 -0
  16. package/dist/{App.9sryp183.js → App.x2d0ygt6.js} +2 -2
  17. package/dist/App.yt9p4nr3.js +20 -0
  18. package/dist/{App.wghtdzsk.js → App.zn4mw16t.js} +1 -1
  19. package/dist/ConnectionsPage.8r96ryw7.js +3 -0
  20. package/dist/McpPage.3cwh0gnd.js +3 -0
  21. package/dist/SettingsPage.ykgdh5ev.js +3 -0
  22. package/dist/SkillsPage.4np1s65b.js +3 -0
  23. package/dist/TasksPage.4g08t7p6.js +3 -0
  24. package/dist/TelemetryPage.72w9pwcp.js +3 -0
  25. package/dist/TestsPage.z4fk3r7r.js +3 -0
  26. package/dist/ThreadsPage.63tcajeh.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +2 -2
  31. package/src/crypto.ts +25 -4
  32. package/src/db.ts +24 -1
  33. package/src/mcp-platform.ts +273 -44
  34. package/src/providers.ts +125 -5
  35. package/src/routes/api/agent-utils.ts +105 -8
  36. package/src/routes/api/providers.ts +64 -0
  37. package/src/routes/api/telemetry.ts +0 -7
  38. package/src/routes/share.ts +3 -2
  39. package/src/server.ts +53 -7
  40. package/src/test-runner.ts +1 -1
  41. package/src/web/App.tsx +37 -22
  42. package/src/web/components/agents/AgentCard.tsx +12 -9
  43. package/src/web/components/agents/AgentPanel.tsx +126 -7
  44. package/src/web/components/agents/AgentsView.tsx +30 -8
  45. package/src/web/components/agents/CreateAgentModal.tsx +155 -5
  46. package/src/web/components/dashboard/Dashboard.tsx +9 -7
  47. package/src/web/components/layout/Sidebar.tsx +43 -32
  48. package/src/web/components/meta-agent/MetaAgent.tsx +6 -2
  49. package/src/web/components/settings/SettingsPage.tsx +172 -43
  50. package/src/web/components/telemetry/TelemetryPage.tsx +54 -46
  51. package/src/web/components/tests/TestsPage.tsx +91 -76
  52. package/src/web/context/TelemetryContext.tsx +4 -1
  53. package/src/web/context/UIModeContext.tsx +49 -0
  54. package/src/web/context/index.ts +3 -0
  55. package/src/web/types.ts +67 -3
  56. package/dist/ActivityPage.sw9p594m.js +0 -3
  57. package/dist/ApiDocsPage.90e03bz7.js +0 -4
  58. package/dist/App.3vnrera5.js +0 -4
  59. package/dist/App.94x6mh7f.js +0 -20
  60. package/dist/App.9t1zc5r7.js +0 -53
  61. package/dist/App.p7jjw1zf.js +0 -4
  62. package/dist/App.pfbdzrhh.js +0 -4
  63. package/dist/App.pse0pzar.js +0 -4
  64. package/dist/App.r43t58w6.js +0 -221
  65. package/dist/App.stgng5bx.js +0 -13
  66. package/dist/App.tm3k7h4b.js +0 -4
  67. package/dist/App.vkg121c6.js +0 -4
  68. package/dist/App.xva0tfzh.js +0 -4
  69. package/dist/App.ysxy7akk.js +0 -61
  70. package/dist/App.yzkh4gq2.js +0 -4
  71. package/dist/ConnectionsPage.q5f9fd37.js +0 -3
  72. package/dist/McpPage.f3ccrezb.js +0 -3
  73. package/dist/SettingsPage.zmzm1pp6.js +0 -3
  74. package/dist/SkillsPage.whxnez67.js +0 -3
  75. package/dist/TasksPage.zp4jfevw.js +0 -3
  76. package/dist/TelemetryPage.an0ky78c.js +0 -3
  77. package/dist/TestsPage.18krj0d1.js +0 -3
  78. package/dist/ThreadsPage.nnphgy98.js +0 -3
@@ -145,6 +145,72 @@ function buildOperatorConfig(features: Agent["features"], projectId: string | nu
145
145
  return operatorResult;
146
146
  }
147
147
 
148
+ function buildRealtimeConfig(features: Agent["features"], projectId: string | null) {
149
+ // Backwards compatible: handle both boolean and RealtimeConfig
150
+ const realtimeVal = features.realtime;
151
+ const isEnabled = typeof realtimeVal === "boolean" ? realtimeVal : (realtimeVal as any)?.enabled ?? false;
152
+
153
+ if (!isEnabled) {
154
+ return { enabled: false, provider: "standard", stt: {}, tts: {} };
155
+ }
156
+
157
+ const rtConfig = typeof realtimeVal === "object" ? realtimeVal as Record<string, unknown> : {};
158
+ const rtProvider = (rtConfig.provider as string) || "standard";
159
+
160
+ // Native OpenAI Realtime mode
161
+ if (rtProvider === "openai") {
162
+ return {
163
+ enabled: true,
164
+ provider: "openai",
165
+ model: (rtConfig.model as string) || "gpt-realtime",
166
+ voice: (rtConfig.voice as string) || "alloy",
167
+ vad_type: (rtConfig.vadType as string) || "semantic_vad",
168
+ };
169
+ }
170
+
171
+ // Native Gemini Live mode
172
+ if (rtProvider === "gemini") {
173
+ return {
174
+ enabled: true,
175
+ provider: "gemini",
176
+ gemini_model: (rtConfig.geminiModel as string) || "",
177
+ gemini_voice: (rtConfig.geminiVoice as string) || "Kore",
178
+ google_search: (rtConfig.googleSearch as boolean) || false,
179
+ };
180
+ }
181
+
182
+ // Standard mode: STT → Core LLM → TTS pipeline
183
+ const sttProvider = (rtConfig.sttProvider as string) || "elevenlabs";
184
+ const sttModel = (rtConfig.sttModel as string) || (sttProvider === "elevenlabs" ? "scribe_v2_realtime" : undefined);
185
+ const ttsProvider = (rtConfig.ttsProvider as string) || "elevenlabs";
186
+ const ttsModel = (rtConfig.ttsModel as string) || undefined;
187
+
188
+ const sttProviderDef = PROVIDERS[sttProvider as ProviderId];
189
+ const ttsProviderDef = PROVIDERS[ttsProvider as ProviderId];
190
+
191
+ const sttConfig: Record<string, unknown> = { provider: sttProvider };
192
+ if (sttModel) sttConfig.model = sttModel;
193
+ // For local providers, include the base URL in the config
194
+ if (sttProviderDef && "isLocal" in sttProviderDef && sttProviderDef.isLocal) {
195
+ sttConfig.base_url = ProviderKeys.getDecryptedForProject(sttProvider, projectId) ||
196
+ ("defaultBaseUrl" in sttProviderDef ? (sttProviderDef as any).defaultBaseUrl : "");
197
+ }
198
+
199
+ const ttsConfig: Record<string, unknown> = { provider: ttsProvider };
200
+ if (ttsModel) ttsConfig.model = ttsModel;
201
+ if (ttsProviderDef && "isLocal" in ttsProviderDef && ttsProviderDef.isLocal) {
202
+ ttsConfig.base_url = ProviderKeys.getDecryptedForProject(ttsProvider, projectId) ||
203
+ ("defaultBaseUrl" in ttsProviderDef ? (ttsProviderDef as any).defaultBaseUrl : "");
204
+ }
205
+
206
+ return {
207
+ enabled: true,
208
+ provider: "standard",
209
+ stt: sttConfig,
210
+ tts: ttsConfig,
211
+ };
212
+ }
213
+
148
214
  // Build agent config from apteva agent data
149
215
  // Note: POST /config expects flat structure WITHOUT "agent" wrapper
150
216
  export function buildAgentConfig(agent: Agent, providerKey: string) {
@@ -302,12 +368,7 @@ export function buildAgentConfig(agent: Agent, providerKey: string) {
302
368
  cache_ttl: "15m",
303
369
  servers: mcpServers,
304
370
  },
305
- realtime: {
306
- enabled: features.realtime,
307
- provider: "openai",
308
- model: "gpt-4o-realtime-preview",
309
- voice: "alloy",
310
- },
371
+ realtime: buildRealtimeConfig(features, agent.project_id),
311
372
  context: {
312
373
  max_messages: 30,
313
374
  max_tokens: 0,
@@ -565,6 +626,41 @@ export async function startAgentProcess(
565
626
  }
566
627
  }
567
628
 
629
+ // If realtime voice is enabled, pass voice provider keys/URLs for STT/TTS
630
+ const rtEnabled = typeof agent.features.realtime === "boolean"
631
+ ? agent.features.realtime
632
+ : (agent.features.realtime as any)?.enabled ?? false;
633
+ if (rtEnabled) {
634
+ // Cloud voice provider keys (backwards compat — always pass if available)
635
+ const elevenlabsKey = ProviderKeys.getDecryptedForProject("elevenlabs", agent.project_id);
636
+ if (elevenlabsKey) env.ELEVENLABS_API_KEY = elevenlabsKey;
637
+ const deepgramKey = ProviderKeys.getDecryptedForProject("deepgram", agent.project_id);
638
+ if (deepgramKey) env.DEEPGRAM_API_KEY = deepgramKey;
639
+
640
+ // Local voice provider URLs
641
+ const localVoiceProviders = ["speaches", "whisper_cpp", "kokoro", "piper", "fish_speech"] as const;
642
+ for (const pvId of localVoiceProviders) {
643
+ const pv = PROVIDERS[pvId as ProviderId];
644
+ if (pv) {
645
+ const url = ProviderKeys.getDecryptedForProject(pvId, agent.project_id);
646
+ if (url) env[pv.envVar] = url;
647
+ }
648
+ }
649
+
650
+ // Pass API keys for native realtime providers (OpenAI/Gemini)
651
+ // Agent may use a different main LLM provider (e.g., Claude) but needs these for voice
652
+ const rtConfig = typeof agent.features.realtime === "object" ? agent.features.realtime as Record<string, unknown> : {};
653
+ const rtProvider = rtConfig.provider as string;
654
+ if (rtProvider === "openai" && agent.provider !== "openai") {
655
+ const openaiKey = ProviderKeys.getDecryptedForProject("openai", agent.project_id);
656
+ if (openaiKey) env.OPENAI_API_KEY = openaiKey;
657
+ }
658
+ if (rtProvider === "gemini" && agent.provider !== "gemini") {
659
+ const geminiKey = ProviderKeys.getDecryptedForProject("gemini", agent.project_id);
660
+ if (geminiKey) env.GEMINI_API_KEY = geminiKey;
661
+ }
662
+ }
663
+
568
664
  // Get binary path dynamically (allows hot-reload of new binary versions)
569
665
  const binaryPath = getBinaryPathForAgent();
570
666
 
@@ -618,8 +714,9 @@ export async function startAgentProcess(
618
714
  console.log(` Pushing configuration...`);
619
715
  }
620
716
  const config = buildAgentConfig(agent, providerKey);
621
- console.log(`[DEBUG] operator config being pushed:`, JSON.stringify(config.operator, null, 2));
622
- console.log(`[DEBUG] builtin_tools:`, JSON.stringify(config.operator?.builtin_tools));
717
+ if (agent.features.realtime) {
718
+ console.log(`[DEBUG] realtime config being pushed:`, JSON.stringify(config.realtime, null, 2));
719
+ }
623
720
  const configResult = await pushConfigToAgent(agent.id, port, config);
624
721
  if (!configResult.success) {
625
722
  if (!silent) {
@@ -172,6 +172,70 @@ export async function handleProviderRoutes(
172
172
  }
173
173
  }
174
174
 
175
+ // ==================== LOCAL VOICE PROVIDERS ====================
176
+
177
+ // GET /api/providers/:id/status - Check if a local voice provider is running
178
+ const localVoiceStatusMatch = path.match(/^\/api\/providers\/(speaches|whisper_cpp|kokoro|piper|fish_speech)\/status$/);
179
+ if (localVoiceStatusMatch && method === "GET") {
180
+ const providerId = localVoiceStatusMatch[1];
181
+ const providerDef = PROVIDERS[providerId as ProviderId];
182
+ const baseUrl = ProviderKeys.getDecrypted(providerId) ||
183
+ ("defaultBaseUrl" in providerDef ? (providerDef as any).defaultBaseUrl : "");
184
+ const isDocker = await isRunningInDocker();
185
+
186
+ const healthEndpoints: Record<string, string> = {
187
+ speaches: "/v1/models",
188
+ whisper_cpp: "/",
189
+ kokoro: "/v1/models",
190
+ piper: "/api/voices",
191
+ fish_speech: "/v1/models",
192
+ };
193
+
194
+ try {
195
+ const response = await fetch(`${baseUrl}${healthEndpoints[providerId]}`, {
196
+ method: "GET",
197
+ signal: AbortSignal.timeout(3000),
198
+ });
199
+ if (response.ok) {
200
+ return json({ connected: true, url: baseUrl, isDocker });
201
+ }
202
+ return json({ connected: false, url: baseUrl, error: "Not responding", isDocker });
203
+ } catch {
204
+ return json({ connected: false, url: baseUrl, error: "Not reachable", isDocker });
205
+ }
206
+ }
207
+
208
+ // GET /api/providers/:id/models - Fetch available models from a local voice provider
209
+ const localVoiceModelsMatch = path.match(/^\/api\/providers\/(speaches|kokoro|fish_speech)\/models$/);
210
+ if (localVoiceModelsMatch && method === "GET") {
211
+ const providerId = localVoiceModelsMatch[1];
212
+ const providerDef = PROVIDERS[providerId as ProviderId];
213
+ const baseUrl = ProviderKeys.getDecrypted(providerId) ||
214
+ ("defaultBaseUrl" in providerDef ? (providerDef as any).defaultBaseUrl : "");
215
+
216
+ try {
217
+ const response = await fetch(`${baseUrl}/v1/models`, {
218
+ method: "GET",
219
+ headers: { "Accept": "application/json" },
220
+ signal: AbortSignal.timeout(5000),
221
+ });
222
+
223
+ if (!response.ok) {
224
+ return json({ error: "Failed to connect", models: [], connected: false }, 200);
225
+ }
226
+
227
+ const data = await response.json() as { data?: Array<{ id: string }> };
228
+ const models = (data.data || []).map((m: { id: string }) => ({
229
+ value: m.id,
230
+ label: m.id,
231
+ }));
232
+
233
+ return json({ models, connected: true });
234
+ } catch {
235
+ return json({ error: "Not reachable", models: [], connected: false }, 200);
236
+ }
237
+ }
238
+
175
239
  // ==================== ONBOARDING ====================
176
240
 
177
241
  // GET /api/onboarding/status - Check onboarding status
@@ -34,13 +34,6 @@ export async function handleTelemetryRoutes(
34
34
  return json({ error: "agent_id and events are required" }, 400);
35
35
  }
36
36
 
37
- // Debug: log raw incoming events
38
- for (const event of body.events) {
39
- if (event.category === "LLM") {
40
- console.log(`[telemetry] RAW LLM event from ${body.agent_id}: ${JSON.stringify(event)}`);
41
- }
42
- }
43
-
44
37
  // Filter out debug events - too noisy
45
38
  const filteredEvents = body.events.filter(e => e.level !== "debug");
46
39
 
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "crypto";
2
- import { AgentDB, type Agent } from "../db";
2
+ import { AgentDB, isRealtimeEnabled, type Agent } from "../db";
3
3
  import { agentFetch } from "./api/agent-utils";
4
4
 
5
5
  function deriveShareToken(apiKey: string, agentId: string): string {
@@ -9,7 +9,7 @@ function deriveShareToken(apiKey: string, agentId: string): string {
9
9
  .substring(0, 32);
10
10
  }
11
11
 
12
- function findAgentByShareToken(token: string): Agent | null {
12
+ export function findAgentByShareToken(token: string): Agent | null {
13
13
  const agents = AgentDB.findAll();
14
14
  for (const agent of agents) {
15
15
  const apiKey = AgentDB.getApiKey(agent.id);
@@ -46,6 +46,7 @@ export async function handleShareRequest(req: Request, path: string): Promise<Re
46
46
  return Response.json({
47
47
  name: agent.name,
48
48
  status: agent.status,
49
+ voiceEnabled: isRealtimeEnabled(agent.features),
49
50
  });
50
51
  }
51
52
 
package/src/server.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { type Server, type Subprocess } from "bun";
2
2
  import { handleApiRequest } from "./routes/api";
3
3
  import { handleAuthRequest } from "./routes/auth";
4
- import { handleShareRequest } from "./routes/share";
4
+ import { handleShareRequest, findAgentByShareToken } from "./routes/share";
5
5
  import { serveStatic } from "./routes/static";
6
6
  import { join } from "path";
7
7
  import { homedir } from "os";
8
8
  import { mkdirSync, existsSync } from "fs";
9
- import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, ChannelDB, TelemetryDB, type McpServer, type Agent } from "./db";
9
+ import { initDatabase, AgentDB, ApiKeyDB, ProviderKeysDB, McpServerDB, ChannelDB, TelemetryDB, UserDB, type McpServer, type Agent } from "./db";
10
10
  import { authMiddleware, type AuthContext } from "./auth/middleware";
11
+ import { verifyAccessToken } from "./auth";
11
12
  import { startMcpProcess } from "./mcp-client";
12
13
  import {
13
14
  ensureBinary,
@@ -371,12 +372,54 @@ const server = Bun.serve({
371
372
  // WebSocket upgrade: /api/agents/:id/voice → proxy to agent binary
372
373
  const voiceMatch = path.match(/^\/api\/agents\/([^/]+)\/voice$/);
373
374
  if (voiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
375
+ // Validate auth via query param token (WebSocket can't send custom headers)
376
+ if (process.env.AUTH_ENABLED !== "false") {
377
+ const token = url.searchParams.get("token");
378
+ if (!token) {
379
+ return new Response("Authentication required", { status: 401 });
380
+ }
381
+ // Try API key first, then JWT
382
+ const isApiKey = token.startsWith("apt_");
383
+ if (isApiKey) {
384
+ const result = ApiKeyDB.validate(token);
385
+ if (!result) {
386
+ return new Response("Invalid API key", { status: 401 });
387
+ }
388
+ } else {
389
+ const payload = verifyAccessToken(token);
390
+ if (!payload) {
391
+ return new Response("Invalid or expired token", { status: 401 });
392
+ }
393
+ const user = UserDB.findById(payload.userId);
394
+ if (!user) {
395
+ return new Response("User not found", { status: 401 });
396
+ }
397
+ }
398
+ }
399
+
374
400
  const agentId = voiceMatch[1];
375
401
  const agent = AgentDB.findById(agentId);
376
402
  if (!agent || agent.status !== "running" || !agent.port) {
377
403
  return new Response("Agent not available", { status: 400 });
378
404
  }
379
- const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port } });
405
+ const agentApiKey = AgentDB.getApiKey(agentId) || "";
406
+ console.log(`[WS] Voice upgrade: agent=${agentId} port=${agent.port} hasKey=${!!agentApiKey}`);
407
+ const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port, agentApiKey } });
408
+ if (upgraded) return undefined;
409
+ return new Response("WebSocket upgrade failed", { status: 500 });
410
+ }
411
+
412
+ // WebSocket upgrade: /share/<token>/voice → proxy to agent binary (public, share-token auth)
413
+ const shareVoiceMatch = path.match(/^\/share\/([a-f0-9]{32})\/voice$/);
414
+ if (shareVoiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
415
+ const shareToken = shareVoiceMatch[1];
416
+ const agent = findAgentByShareToken(shareToken);
417
+ if (!agent || agent.status !== "running" || !agent.port) {
418
+ return new Response("Agent not available", { status: 400 });
419
+ }
420
+ const agentApiKey = AgentDB.getApiKey(agent.id) || "";
421
+ console.log(`[WS] Share voice upgrade: agent=${agent.id} port=${agent.port}`);
422
+ const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port, agentApiKey } });
380
423
  if (upgraded) return undefined;
381
424
  return new Response("WebSocket upgrade failed", { status: 500 });
382
425
  }
@@ -454,8 +497,10 @@ const server = Bun.serve({
454
497
  // WebSocket proxy for agent voice/realtime
455
498
  websocket: {
456
499
  open(ws: any) {
457
- const { agentId, agentPort } = ws.data;
458
- const agentWs = new WebSocket(`ws://localhost:${agentPort}/voice`);
500
+ const { agentId, agentPort, agentApiKey } = ws.data;
501
+ const wsTarget = `ws://localhost:${agentPort}/voice${agentApiKey ? `?api_key=${agentApiKey}` : ''}`;
502
+ console.log(`[WS] Voice proxy connecting: agent=${agentId} port=${agentPort}`);
503
+ const agentWs = new WebSocket(wsTarget);
459
504
 
460
505
  agentWs.onopen = () => {
461
506
  console.log(`[WS] Voice proxy connected: agent=${agentId} port=${agentPort}`);
@@ -464,10 +509,11 @@ const server = Bun.serve({
464
509
  try { ws.send(event.data); } catch {}
465
510
  };
466
511
  agentWs.onclose = (event: CloseEvent) => {
467
- console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code}`);
512
+ console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code} reason=${event.reason}`);
468
513
  ws.close(event.code, event.reason);
469
514
  };
470
- agentWs.onerror = () => {
515
+ agentWs.onerror = (err: any) => {
516
+ console.log(`[WS] Agent WS error: agent=${agentId}`, err?.message || err);
471
517
  ws.close(1011, "Agent WebSocket error");
472
518
  };
473
519
 
@@ -241,7 +241,7 @@ export async function runTest(testCase: TestCase): Promise<TestRun> {
241
241
 
242
242
  const chatRes = await agentFetch(runningAgent.id, runningAgent.port!, "/chat", {
243
243
  method: "POST",
244
- headers: { "Content-Type": "application/json" },
244
+ headers: { "Content-Type": "application/json", "X-Test-Mode": "true" },
245
245
  body: JSON.stringify(chatBody),
246
246
  });
247
247
 
package/src/web/App.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useMemo, lazy, Suspense } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
- import { Chat } from "@apteva/apteva-kit";
3
+ import { Chat, Call } from "@apteva/apteva-kit";
4
4
  import "@apteva/apteva-kit/styles.css";
5
5
 
6
6
  // Types
@@ -8,7 +8,7 @@ import type { Agent, Provider, Route, NewAgentForm } from "./types";
8
8
  import { DEFAULT_FEATURES } from "./types";
9
9
 
10
10
  // Context
11
- import { TelemetryProvider, AuthProvider, ProjectProvider, ThemeProvider, useTheme, useAuth, useProjects, useAgentStatusChange, useTaskChange } from "./context";
11
+ import { TelemetryProvider, AuthProvider, ProjectProvider, ThemeProvider, UIModeProvider, useTheme, useAuth, useProjects, useAgentStatusChange, useTaskChange } from "./context";
12
12
 
13
13
  // Hooks
14
14
  import { useAgents, useProviders, useOnboarding } from "./hooks";
@@ -348,6 +348,7 @@ function SharePage({ token }: { token: string }) {
348
348
  const { theme } = useTheme();
349
349
  const [status, setStatus] = useState<"checking" | "online" | "offline">("checking");
350
350
  const [agentName, setAgentName] = useState("Agent");
351
+ const [voiceEnabled, setVoiceEnabled] = useState(false);
351
352
 
352
353
  useEffect(() => {
353
354
  const check = async () => {
@@ -356,6 +357,7 @@ function SharePage({ token }: { token: string }) {
356
357
  if (res.ok) {
357
358
  const data = await res.json();
358
359
  setAgentName(data.name || "Agent");
360
+ setVoiceEnabled(!!data.voiceEnabled);
359
361
  setStatus(data.status === "running" ? "online" : "offline");
360
362
  } else {
361
363
  setStatus("offline");
@@ -392,17 +394,28 @@ function SharePage({ token }: { token: string }) {
392
394
  return (
393
395
  <div className="min-h-[100dvh] flex items-center justify-center p-0 md:p-4" style={{ backgroundColor: "var(--color-bg)" }}>
394
396
  <div className="w-full max-w-[640px] h-[100dvh] md:h-[calc(100dvh-32px)] md:max-h-[800px] md:rounded-xl overflow-hidden md:border flex flex-col" style={{ backgroundColor: "var(--color-bg)", borderColor: "var(--color-border)" }}>
395
- <Chat
396
- agentId="default"
397
- apiUrl={`/share/${token}`}
398
- placeholder="Type a message..."
399
- variant="terminal"
400
- theme={theme.id as "light" | "dark"}
401
- headerTitle={agentName}
402
- enableMarkdown
403
- enableWidgets
404
- availableWidgets={["form", "kpi"]}
405
- />
397
+ {voiceEnabled ? (
398
+ <Call
399
+ agentId="default"
400
+ agentName={agentName}
401
+ apiUrl={`/share/${token}`}
402
+ variant="default"
403
+ theme={theme.id as "light" | "dark"}
404
+ showTranscript={false}
405
+ />
406
+ ) : (
407
+ <Chat
408
+ agentId="default"
409
+ apiUrl={`/share/${token}`}
410
+ placeholder="Type a message..."
411
+ variant="terminal"
412
+ theme={theme.id as "light" | "dark"}
413
+ headerTitle={agentName}
414
+ enableMarkdown
415
+ enableWidgets
416
+ availableWidgets={["form", "kpi"]}
417
+ />
418
+ )}
406
419
  </div>
407
420
  </div>
408
421
  );
@@ -422,15 +435,17 @@ function App() {
422
435
 
423
436
  return (
424
437
  <ThemeProvider>
425
- <AuthProvider>
426
- <ProjectProvider>
427
- <MetaAgentProvider>
428
- <TelemetryProvider>
429
- <AppContent />
430
- </TelemetryProvider>
431
- </MetaAgentProvider>
432
- </ProjectProvider>
433
- </AuthProvider>
438
+ <UIModeProvider>
439
+ <AuthProvider>
440
+ <ProjectProvider>
441
+ <MetaAgentProvider>
442
+ <TelemetryProvider>
443
+ <AppContent />
444
+ </TelemetryProvider>
445
+ </MetaAgentProvider>
446
+ </ProjectProvider>
447
+ </AuthProvider>
448
+ </UIModeProvider>
434
449
  </ThemeProvider>
435
450
  );
436
451
  }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, SkillsIcon, ActivityIcon } from "../common/Icons";
3
- import { useAgentActivity, useProjects } from "../../context";
3
+ import { useAgentActivity, useProjects, useUIMode } from "../../context";
4
4
  import type { Agent, AgentFeatures } from "../../types";
5
5
 
6
6
  interface AgentCardProps {
@@ -28,6 +28,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
28
28
  const skills = agent.skillDetails || [];
29
29
  const { isActive, label: activityLabel } = useAgentActivity(agent.id);
30
30
  const { projects } = useProjects();
31
+ const { isDev } = useUIMode();
31
32
  const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
32
33
  const subscriptions = agent.subscriptions || [];
33
34
 
@@ -43,10 +44,12 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
43
44
  <div className="flex items-start justify-between mb-3">
44
45
  <div>
45
46
  <h3 className="font-semibold text-lg">{agent.name}</h3>
46
- <p className="text-sm text-[var(--color-text-muted)]">
47
- {agent.provider} / {agent.model}
48
- {agent.port && <span className="text-[var(--color-text-faint)]"> · :{agent.port}</span>}
49
- </p>
47
+ {isDev && (
48
+ <p className="text-sm text-[var(--color-text-muted)]">
49
+ {agent.provider} / {agent.model}
50
+ {agent.port && <span className="text-[var(--color-text-faint)]"> · :{agent.port}</span>}
51
+ </p>
52
+ )}
50
53
  {showProject && project && (
51
54
  <p className="text-sm text-[var(--color-text-muted)] flex items-center gap-1.5 mt-1">
52
55
  <span className="w-2 h-2 rounded-full" style={{ backgroundColor: project.color }} />
@@ -57,7 +60,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
57
60
  <StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityLabel={activityLabel} />
58
61
  </div>
59
62
 
60
- {enabledFeatures.length > 0 && (
63
+ {isDev && enabledFeatures.length > 0 && (
61
64
  <div className="flex flex-wrap gap-1.5 mb-3">
62
65
  {enabledFeatures.map(({ key, icon: Icon, label }) => (
63
66
  <span
@@ -74,7 +77,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
74
77
  )}
75
78
 
76
79
  {/* MCP Servers */}
77
- {mcpServers.length > 0 && (
80
+ {isDev && mcpServers.length > 0 && (
78
81
  <div className="flex flex-wrap gap-1.5 mb-3">
79
82
  {mcpServers.map((server) => {
80
83
  // HTTP/remote servers are always available
@@ -98,7 +101,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
98
101
  )}
99
102
 
100
103
  {/* Skills */}
101
- {skills.length > 0 && (
104
+ {isDev && skills.length > 0 && (
102
105
  <div className="flex flex-wrap gap-1.5 mb-3">
103
106
  {skills.map((skill) => (
104
107
  <span
@@ -118,7 +121,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
118
121
  )}
119
122
 
120
123
  {/* Subscriptions (triggers listening to) */}
121
- {subscriptions.length > 0 && (
124
+ {isDev && subscriptions.length > 0 && (
122
125
  <div className="flex flex-wrap gap-1.5 mb-3">
123
126
  {subscriptions.map((sub) => (
124
127
  <span