diff --git a/packages/app/public/android-chrome-192x192.png b/packages/app/public/android-chrome-192x192.png new file mode 100644 index 0000000000000..a5db6b9c58f67 Binary files /dev/null and b/packages/app/public/android-chrome-192x192.png differ diff --git a/packages/app/public/android-chrome-512x512.png b/packages/app/public/android-chrome-512x512.png new file mode 100644 index 0000000000000..844c128d278d7 Binary files /dev/null and b/packages/app/public/android-chrome-512x512.png differ diff --git a/packages/app/public/apple-touch-icon.png b/packages/app/public/apple-touch-icon.png new file mode 100644 index 0000000000000..4d18026b82e6a Binary files /dev/null and b/packages/app/public/apple-touch-icon.png differ diff --git a/packages/app/public/brand/favicons/android-chrome-192x192.png b/packages/app/public/brand/favicons/android-chrome-192x192.png index b5f72dae4d0ae..a5db6b9c58f67 100644 Binary files a/packages/app/public/brand/favicons/android-chrome-192x192.png and b/packages/app/public/brand/favicons/android-chrome-192x192.png differ diff --git a/packages/app/public/brand/favicons/android-chrome-512x512.png b/packages/app/public/brand/favicons/android-chrome-512x512.png index 2ef4a319fd4e3..844c128d278d7 100644 Binary files a/packages/app/public/brand/favicons/android-chrome-512x512.png and b/packages/app/public/brand/favicons/android-chrome-512x512.png differ diff --git a/packages/app/public/brand/favicons/app-icon-192.png b/packages/app/public/brand/favicons/app-icon-192.png new file mode 100644 index 0000000000000..a5db6b9c58f67 Binary files /dev/null and b/packages/app/public/brand/favicons/app-icon-192.png differ diff --git a/packages/app/public/brand/favicons/app-icon-512.png b/packages/app/public/brand/favicons/app-icon-512.png new file mode 100644 index 0000000000000..844c128d278d7 Binary files /dev/null and b/packages/app/public/brand/favicons/app-icon-512.png differ diff --git a/packages/app/public/brand/favicons/apple-touch-icon.png b/packages/app/public/brand/favicons/apple-touch-icon.png index 62f919c8fec08..4d18026b82e6a 100644 Binary files a/packages/app/public/brand/favicons/apple-touch-icon.png and b/packages/app/public/brand/favicons/apple-touch-icon.png differ diff --git a/packages/app/public/site.webmanifest b/packages/app/public/site.webmanifest index c9ef00c6a2133..a30d152d6df29 100644 --- a/packages/app/public/site.webmanifest +++ b/packages/app/public/site.webmanifest @@ -18,14 +18,16 @@ "type": "image/x-icon" }, { - "src": "/brand/favicons/android-chrome-192x192.png", + "src": "/brand/favicons/app-icon-192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" }, { - "src": "/brand/favicons/android-chrome-512x512.png", + "src": "/brand/favicons/app-icon-512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" } ] -} +} \ No newline at end of file diff --git a/packages/app/src/main.tsx b/packages/app/src/main.tsx index f8f3ec4995bf0..e27e14ba179e8 100644 --- a/packages/app/src/main.tsx +++ b/packages/app/src/main.tsx @@ -1,27 +1,9 @@ -/** - * Renderer boot entry and composition root for the cross-platform Eliza app - * shell (web browser, Electrobun desktop, and Capacitor iOS/Android). Runs - * before React mounts: starts cold-start telemetry, registers host-external - * view importers, and resolves cloud-only branding from the injected API base - * / desktop runtime mode. - * - * `main()` drives the boot pipeline — embed-iframe session handshake, app-window - * and model-tester route shortcuts, managed cloud launch connection, the - * headless iOS full-Bun backend smoke gate, popout and detached/overlay window - * shells, then the per-platform bridge stack (storage + Capacitor bridges, iOS - * local-agent fetch/native-request bridges, Android native agent fetch bridge, - * screen-capture / OCR / voice harnesses) — before mounting the React tree - * (`@elizaos/ui` App, optionally wrapped by the web-only CloudRouterShell) and - * running `initializePlatform()` concurrently after paint. - * - * Also owns deep-link handling (custom `://` + `eliza.app` universal - * links → hash routes, navigate-view events, or first-run remote connect), the - * trusted-apiBase / native-WebSocket URL policy (tightened for iOS store + cloud - * builds; a bearer token is never accepted from an OS deep link), the mobile - * device bridge + agent tunnel + background runner, and the desktop tray / - * global-shortcut / chat-overlay wiring. Modules not needed for first paint are - * deferred onto the idle path. Exports the resolved platform flags. - */ +// FIRST side-effect: repair the same-origin WebSocket base for the plain-web +// served bundle before the `client` singleton can dial its socket. The dev +// server injects a desktop-loopback `__ELIZA_WS_BASE__` (ws://127.0.0.1:31337) +// that client-base reads first; on a reverse-proxied web page the socket must +// be same-origin (wss:///ws). No-op on desktop / native. See module. +import "./web-ws-base-fix"; import { ErrorBoundary } from "@elizaos/ui/components/ui/error-boundary"; import "@elizaos/ui/styles"; // Native-only (ios/android/desktop): register the Eliza Cloud Applications @@ -174,7 +156,6 @@ import { type IosRuntimeConfig, resolveIosRuntimeConfig, } from "./ios-runtime"; -import { startKeyboardDictationSession } from "./keyboard-dictation"; import { createMobileLifecycle, type MobileLifecycle, @@ -1790,12 +1771,6 @@ function handleDeepLink(url: string): void { // is consumed by installAecLoopHarness's hashchange watcher. setHashRoute("aec-loop", parsed.searchParams); break; - case "keyboard-dictation": - // iOS keyboard app-handoff dictation (#12185): extensions have no mic, - // so the ElizaKeyboard extension opens the app; record + transcribe - // here, publish the transcript to the App Group, keyboard inserts it. - startKeyboardDictationSession(parsed.searchParams); - break; case "connect": { const gatewayUrl = parsed.searchParams.get("url"); if (gatewayUrl) { diff --git a/packages/app/src/web-ws-base-fix.ts b/packages/app/src/web-ws-base-fix.ts new file mode 100644 index 0000000000000..56bbddd80d808 --- /dev/null +++ b/packages/app/src/web-ws-base-fix.ts @@ -0,0 +1,174 @@ +/** + * Same-origin API + WebSocket base repair for the PLAIN-WEB served bundle. + * + * Context: when the app is served by the Vite dev server as a plain browser page + * (NOT the electrobun desktop shell, NOT a Capacitor native webview), the dev + * `appDevWsBasePlugin` injects `window.__ELIZA_WS_BASE__ = "ws://127.0.0.1:"` + * into the served HTML (apiPort defaults to 31337, the desktop loopback API). + * + * `client-base.ts` `getInjectedWsBase()` reads that global FIRST — before it + * would otherwise derive the socket host from `window.location`. So even with an + * empty (same-origin) REST base, the realtime socket dials the dead desktop + * loopback `ws://127.0.0.1:31337/ws`, which is refused, and live chat never + * connects. + * + * When the page is actually served over http/https from a real remote host that + * a reverse proxy (nginx) fronts — proxying `/ws` and `/api` to the backend with + * auth injected — the correct socket target is same-origin + * `wss:///ws` and REST is same-origin `/api`. + * + * Two things must be corrected for the plain-web path, both UPSTREAM of the + * DO-NOT-EDIT client-base.ts: + * + * 1. WS base: rewrite the injected desktop-loopback `__ELIZA_WS_BASE__` to + * same-origin `wss://` so `getInjectedWsBase()` resolves the correct + * socket host. + * + * 2. REST base: set `__ELIZA_API_BASE__` to same-origin `https://` so + * `this.baseUrl` is NON-EMPTY. This is required because client-base's + * `connectWs()` has a guard that BAILS when `baseUrl` is empty AND the page + * host has no port and isn't loopback (a Capacitor synthetic-host + * protection). A plain remote https host like `sol-overhaul.shad0w.xyz` + * (portless, non-loopback) trips that guard, so an empty REST base leaves + * the socket un-opened even with a correct WS base. A same-origin absolute + * REST base is equivalent to relative `/api` (nginx proxies it with the + * injected auth header) and makes the guard pass so the socket opens. + * + * Scope: this only mutates `getElizaApiBase()`'s globals (`__ELIZA_API_BASE__` / + * `__ELIZAOS_API_BASE__`), which the REST client reads. It deliberately does NOT + * touch `__ELIZA_APP_API_BASE__` / the branded `___API_BASE__` that + * `getInjectedAppApiBase()` reads for cloud-only branding — so app branding is + * unaffected. + * + * Desktop (electrobun) and native (Capacitor) contexts are left untouched — they + * legitimately need the injected / native base. + * + * This module MUST be imported as the first side-effect in `main.tsx`, before + * the `client` singleton's `connectWs()` can run. + */ +import { Capacitor } from "@capacitor/core"; +import { isElectrobunRuntime } from "@elizaos/ui/bridge"; + +const LOOPBACK_HOSTNAMES = new Set([ + "localhost", + "127.0.0.1", + "::1", + "[::1]", + "0.0.0.0", +]); + +function isLoopbackHostname(hostname: string): boolean { + return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase()); +} + +function setInjectedGlobal(key: string, value: string): void { + try { + const w = window as unknown as Record; + w[key] = value; + } catch { + // best-effort — never block boot + } +} + +/** + * Same-origin realtime socket base for the current page: + * `wss://` on https, `ws://` on http. client-base appends `/ws` + * and the clientId/token query itself, so only the origin (protocol + host) + * needs to be correct here. + */ +function sameOriginWsBase(): string { + const loc = window.location; + const proto = loc.protocol === "https:" ? "wss:" : "ws:"; + return `${proto}//${loc.host}`; +} + +/** Same-origin REST API base for the current page: `https://`. */ +function sameOriginRestBase(): string { + const loc = window.location; + return `${loc.protocol}//${loc.host}`; +} + +/** + * Returns true only for the plain-web served context that should use a + * same-origin API/socket (not desktop, not native, page on a real http/https + * non-loopback host). + */ +function isPlainWebSameOriginContext(): boolean { + if (typeof window === "undefined") return false; + // Desktop shell needs the injected loopback API base. + if (isElectrobunRuntime()) return false; + // Capacitor iOS/Android use their own native/injected bases. + try { + if (Capacitor.isNativePlatform()) return false; + } catch { + // If Capacitor isn't resolvable treat as web; fall through. + } + const loc = window.location; + if (loc.protocol !== "http:" && loc.protocol !== "https:") return false; + // Loopback page host = an actual local dev-in-browser session pointed at the + // real loopback API; leave the injection alone there. + if (isLoopbackHostname(loc.hostname)) return false; + return true; +} + +function injectedWsBaseIsForeignLoopback(value: unknown): boolean { + if (typeof value !== "string" || !value.trim()) return false; + try { + const parsed = new URL(value); + if (parsed.protocol !== "ws:" && parsed.protocol !== "http:") { + // A wss:/https: injection already implies a real proxied host; don't + // second-guess it. + return false; + } + // ws:/http: injection is the desktop-loopback default; on a plain-web + // remote page it is always wrong. + return true; + } catch { + return false; + } +} + +/** + * Repoint the dev-injected desktop-loopback API + WS bases at the current + * (reverse-proxied) origin on the plain-web path so REST hits same-origin + * `/api` and the realtime socket dials `wss:///ws`. No-op on desktop / + * native / loopback-dev contexts. + */ +export function repairWebSameOriginWsBase(): void { + if (!isPlainWebSameOriginContext()) return; + const w = window as unknown as { + __ELIZA_WS_BASE__?: unknown; + __ELIZAOS_WS_BASE__?: unknown; + }; + const anyForeign = + injectedWsBaseIsForeignLoopback(w.__ELIZA_WS_BASE__) || + injectedWsBaseIsForeignLoopback(w.__ELIZAOS_WS_BASE__); + if (!anyForeign) return; + + // 1) WS base → same-origin wss://. + const wsTarget = sameOriginWsBase(); + setInjectedGlobal("__ELIZA_WS_BASE__", wsTarget); + setInjectedGlobal("__ELIZAOS_WS_BASE__", wsTarget); + try { + const wRecord = window as unknown as Record; + for (const key of Object.keys(wRecord)) { + if ( + /^__[A-Z0-9]+_WS_BASE__$/.test(key) && + injectedWsBaseIsForeignLoopback(wRecord[key]) + ) { + setInjectedGlobal(key, wsTarget); + } + } + } catch { + // best-effort + } + + // 2) REST base → same-origin https://, so the client's baseUrl is + // non-empty and connectWs()'s empty-baseUrl guard does not bail. Only the + // getElizaApiBase() globals — NOT the app-branding globals. + const restTarget = sameOriginRestBase(); + setInjectedGlobal("__ELIZA_API_BASE__", restTarget); + setInjectedGlobal("__ELIZAOS_API_BASE__", restTarget); +} + +repairWebSameOriginWsBase(); diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index bd933584d0299..db1677cac777f 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -2428,6 +2428,23 @@ export function App() { "max(calc(var(--safe-area-top, 0px) - 1.25rem), 1.25rem)", }} > + {/* BOTTOM-BAR / SAFE-AREA FLOOR (do not remove): a viewport-filling + dark background-token floor mounted on EVERY route, behind the + shader (z-0) and every other layer. html/body/#root paint the + orange launch guard (--launch-bg #ef5a1f) as a FOUC color, and on + shared-background routes (home/chat) the AppBackground shader was + the ONLY thing hiding it. On iOS the composer overlay is anchored + by the visualViewport-derived `bottom`, so in the home-indicator + safe-area the shader coverage can fall short and the orange host + color bled through as a band under the composer. This floor makes + the bottom inset (and every unpainted zone) the dark BACKGROUND + token — never accent — regardless of route or shader state. The + shader/wallpaper renders on top of it unchanged on shared routes. */} +