01 · System Overview & Frontend
First installment of the architecture deep-dive: system topology, Electron main process, Vue 3 renderer, and the communication link to the Bridge. Corresponds to
ARCHITECTURE.md§1–§2.
§1 System Topology
┌─────────────────────────────────────────────────────────────────┐
│ Electron Main Process (main.js) │
│ ├─ BrowserWindow (main window / popup chat / QuickAsk / PhoneCall)│
│ ├─ Tray (hide-on-close; quit only via tray menu) │
│ ├─ globalShortcut / clipboard / protocol handler │
│ ├─ autoUpdater (electron-updater) │
│ └─ spawn child: python -m src.bridge.run │
└──────────────────────┬──────────────────────────────────────────┘
│ HTTPS/HTTP2, 127.0.0.1:18711 (default)
┌──────────────────────┴──────────────────────────────────────────┐
│ Python Bridge (src/bridge/run.py → src/bridge/app.py) │
│ ├─ hypercorn + HTTP/2 (self-signed cert workspace/.bridge.crt) │
│ ├─ FastAPI + 60+ routes (src/bridge/routes/*.py) │
│ ├─ APScheduler (cron / schedule / reminder) │
│ ├─ Local small models (rewriter / decider / memorizer-writer) │
│ └─ Tool system (browser / DH / multimodal / skill / …) │
└──────────────────────┬──────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
OpenAI-compatible Local models External
LLM (bge-m3, services
(BASE_URL/API_KEY) rewriter, (GPT-SoVITS,
decider) browser CDP,
social platforms)Why Windows-first: CUDA / Windows WDDM is sensitive to first-touch threading; Chrome CDP shares a single Profile; Windows toast / AUMID penetrates Focus Assist. macOS branches exist but are incomplete (src/base/auth/device.py has a real implementation only for macOS; Windows is stuck on a hostname-only fingerprint).
§2 Electron Main Process (electron/electron/main.js)
2.1 Process Model
main.js is the Electron main-process entry point. It requires the key modules:
const { startPythonBridge, stopPythonBridge, isBridgeOurChild,
getBridgePort, getBridgeToken, onBridgePortChange } = require('./python')
const { mountTray, destroyTray } = require('./tray')
const { grabSelectedTextViaCopy } = require('./clipboard_grab')
const { createQuickAskWindow, getQuickAskWindow } = require('./quick_ask_window')
const { createPhoneCallWindow, getPhoneCallWindow } = require('./phone_call_window')
const { bridgeUrl, bridgeFetch } = require('./bridgeHttp')Window inventory:
| Window | Variable | Purpose |
|---|---|---|
| Main window | mainWindow | Main Vue SPA |
| Overlay | overlayWindow | Screen overlay |
| QuickAsk | quickAskWindow | Hotkey-triggered question popup |
| PhoneCall | phoneCallWindow | Voice call |
| Chat popup | chatWindowsByKey (Map) | Deduplicated by "<agent>:<dialogue>" key |
| Child window set | childWindows (Set) | Tracks all popups; cleared on close |
Hide-on-close: Closing the main window → isQuitting=false → event.preventDefault() → hide the window; quitting is triggered only through the tray menu "Exit Backend" calling app.quit(). The first hide fires a Notification telling the user where the tray icon is; a marker file .tray-hint-shown prevents repeat notifications.
2.2 EPIPE Protection
// Top-of-file IIFE in main.js:
for (const stream of [process.stdout, process.stderr]) {
stream.on('error', () => {}) // swallow async EPIPE
const orig = stream.write.bind(stream)
stream.write = function (...args) { // wrap synchronous write
try { return orig(...args) } catch { return false }
}
}
process.on('uncaughtException', (e) => { if (isEpipe(e)) return; throw e })
process.on('unhandledRejection', (e) => { if (isEpipe(e)) return; throw e })When the parent terminal closes, stdout/stderr trigger EPIPE → three layers of protection (event listener, synchronous wrapper, uncaught-exception handler).
§3 Bridge Lifecycle (electron/electron/python.js)
3.1 Port Decision
const PORTFILE = path.join(PROJECT_ROOT, 'workspace', '.bridge.port')
const TOKENFILE = path.join(PROJECT_ROOT, 'workspace', '.bridge_token')- Check 18711 first:
isPortFree(18711)— if free, use the default port (keeps the autorun watchdog stable) - If occupied:
pickFreePort()— bind127.0.0.1:0, OS assigns a random port, then release - Health probe:
pingHealth(port, timeoutMs=2000)— poll/healthat 250ms intervals - Deep probe:
deepProbe(port)— hits a FastAPI worker-pool endpoint to confirm asyncio scheduling is healthy - Write port file:
workspace/.bridge.port+workspace/.bridges.json
3.2 Python Resolution
function resolvePython(projectRoot) {
if (process.env.FAMILIARS_PYTHON) return process.env.FAMILIARS_PYTHON
const venv = process.platform === 'win32'
? path.join(projectRoot, '.venv', 'Scripts', 'python.exe')
: path.join(projectRoot, '.venv', 'bin', 'python')
if (fs.existsSync(venv)) return venv
return process.platform === 'win32' ? 'python' : 'python3'
}Priority: FAMILIARS_PYTHON env var > .venv/Scripts/python.exe > system python.
3.3 Launch Arguments
python -m src.bridge.runEnvironment variables:
BRIDGE_PORT(default 18711)FAMILIARS_HTTP_INSECURE=1→ fall back to uvicorn + HTTPFAMILIARS_DAEMON=1→ autorun mode (pidfile lock + log redirection)
§4 Vue 3 Renderer (electron/renderer/)
4.1 Tech Stack
- Vue 3 + TypeScript + Pinia
- Vite build (dev server at
http://localhost:5173) - VueFlow (flow-graph editor)
- axios (HTTP + SSE; preload exposes
window.familiarsAPI)
4.2 Directory Structure
electron/renderer/src/
├── assets/ # Static resources (mascot PNGs, etc.)
├── components/
│ ├── Desktop.vue # New default desktop shell (manager landing)
│ ├── ManagerLanding.vue # Legacy landing; activated by localStorage.familiars:legacyLanding
│ ├── ChatBubble.vue # Chat bubble (with emit_event rich rendering)
│ ├── CreativeWorkshop/ # Workshop UI
│ ├── FlowEditor/ # Flow editor V2
│ ├── BrokerMarketplace/ # Marketplace UI
│ └── Ventaris/ # Guild system
├── stores/ # Pinia stores
├── router/ # Vue Router
└── main.ts # Entry point4.3 Renderer ↔ Bridge Communication
renderer → preload → main.js → bridge
↓
window.familiars.bridgeFetch(url, init)
↓
bridgeUrl(port, path) → https://127.0.0.1:{port}{path}CORS allowlist (src/bridge/app.py):
allow_origins=[
"http://localhost:5173", # Vite dev
"http://127.0.0.1:5173", # Vite alt
"null", # file:// (packaged build)
]Token authentication: X-Bridge-Token header (axios) or ?token=... query param (SSE / WS / <img src>). Exempt paths: /health, /api/auth/*, /api/broker_app/* (iframe static assets), /api/demo/*, /api/chat/* (demo SPA uses demo token).
4.4 Hot Port Update
onBridgePortChange((newPort, newToken) => {
// Reconfigure bridgeFetch base URL
// Re-inject preload token
})When the bridge restarts on a different port, python.js fires _portChangeListeners → the renderer hot-switches.
§5 Key Design Decisions
| Decision | Rationale |
|---|---|
| Single-process bridge | Memory sharing, no IPC complexity, CUDA first-touch thread safety |
| HTTPS / HTTP/2 | Self-signed cert workspace/.bridge.crt (src/bridge/certs.py); HTTP/2 multiplexing solves Chromium's 6-socket-per-host limit |
| Electron over Tauri | Reuse system APIs (tray / auto-start / shared browser profile), fewer desktop constraints |
| No LangChain / AutoGen wrapper | Custom pipeline treats "plan → tool → memory → report" as first-class citizens; local small models are deeply coupled |
| Port file over hardcoding | Autorun watchdog always uses 18711; Electron probes then picks a port; CLI reads the port file |
Next: 02 - Bridge Core Layer →
