IPC and Communication
Magia has four communication layers between its parts:
- Tauri
invoke()— frontend → backend request/response - Tauri events — backend → frontend fire-and-forget (and vice versa)
- Unix sockets — external processes → backend (agents, hook-handler, CLI)
- WebSocket — IDE extensions → backend
Tauri invoke() — commands
Section titled “Tauri invoke() — commands”The frontend calls invoke(commandName, args) from @tauri-apps/api/core. Each command is a Rust function annotated with #[tauri::command] and registered in the Tauri builder.
// Frontendimport { invoke } from "@tauri-apps/api/core";
const sessions = await invoke<Session[]>("get_active_sessions");// Backend (src-tauri/src/…)#[tauri::command]fn get_active_sessions() -> Vec<ActiveSession> { … }Naming convention
Section titled “Naming convention”Tauri translates between camelCase (JavaScript) and snake_case (Rust) automatically. By default, Rust snake_case parameter names become camelCase on the JS side. Commands annotated with #[tauri::command(rename_all = "snake_case")] expect snake_case argument keys from JavaScript — be careful to match this when calling such commands.
Mismatched naming causes silent failures (the command succeeds but receives None / default values for every parameter). Always add error logging to invoke calls during development.
Key command groups
Section titled “Key command groups”| Group | Example commands |
|---|---|
| Session management | get_active_sessions, create_session, update_session, delete_session, set_session_title |
| Agent lifecycle | start_agent, stop_agent, send_input, get_agent_events |
| PTY / terminal | create_pty, write_pty, resize_pty, kill_pty |
| Discovery | get_projects, get_sessions_for_project, rescan_project |
| Settings | load_settings, save_settings, get_otel_port |
| Provider config | read_claude_settings, update_claude_settings, read_gemini_settings, inject_gemini_hooks |
| Provider accounts | get_provider_accounts, connect_provider, disconnect_provider |
| File operations | read_file, write_file, list_directory |
| Search | search_sessions, search_files |
| Auth / cloud | store_auth_token, get_auth_token, get_current_user, api_get, api_request |
| System | start_background_sync, is_first_launch, factory_reset, get_api_url |
| Stats / metrics | get_session_metrics, get_project_stats |
| Plugins | list_plugins, install_plugin, enable_plugin, disable_plugin |
Tauri events — real-time updates
Section titled “Tauri events — real-time updates”Events flow in both directions but are predominantly backend → frontend. The backend calls app_handle.emit(event_name, payload). The frontend listens with listen(event_name, handler) from @tauri-apps/api/event.
import { listen } from "@tauri-apps/api/event";
const unlisten = await listen<AgentEvent>("agent:event", (event) => { useAgentEventsStore.getState().addEvent(event.payload);});// Call unlisten() to deregisterEvent catalogue
Section titled “Event catalogue”| Event | Direction | Description |
|---|---|---|
agent:event | Backend → Frontend | A new hook event from a running agent (assistant message, tool call, tool result, permission request, etc.) |
live-sessions:update | Backend → Frontend | The map of currently running agent sessions has changed |
claude-data:projects-invalidated | Backend → Frontend | The session cache has changed; frontend should refetch discovery data |
sessions:changed | Backend → Frontend | Session metadata changed (title, status, hidden flag, etc.) |
otel:metrics-update | Backend → Frontend | New OTel metrics (token counts, latencies) for a session |
settings:changed | Backend → Frontend | Settings file changed on disk (e.g. by a watcher) |
providers:changed | Backend → Frontend | A provider binary was installed or removed |
live-sessions:update | Backend → Frontend | Active sessions map changed (PID check, new session, session ended) |
notification-session-clicked | Backend → Frontend | User clicked a native OS notification; payload is the session ID |
tray:open-project | Backend → Frontend | User opened a project from the tray menu |
tray:new-session | Backend → Frontend | User clicked “New Session” in the tray |
tray:minimized-first-time | Backend → Frontend | App was minimised to tray for the first time |
tray:theme-changed | Frontend → Backend | Dark/light theme toggled; tray panel window syncs |
tray:sync-completed | Backend → Frontend | Session cache periodic sync finished |
session:hijacked | Backend → Frontend | Another process has taken control of the active session |
factory-reset-complete | Backend → Frontend | Factory reset completed (dev mode only; prod uses app.restart()) |
Unix sockets — external process communication
Section titled “Unix sockets — external process communication”The Rust backend listens on several Unix sockets in $TMPDIR/magia-{uid}/. All socket paths are managed by the paths module.
Hook listener
Section titled “Hook listener”Purpose: Receives lifecycle events from agent CLI processes (Claude Code, Gemini, Codex) as they run.
Protocol: The agent CLI is configured to call magia-hook-handler (a small helper binary) for each hook event. magia-hook-handler connects to the hook socket and writes a newline-delimited JSON payload. The hook listener reads the payload, passes it through the event_normalizer, and emits an agent:event Tauri event to the frontend.
Agent CLI → exec magia-hook-handler (per event) → Unix socket (hook path) → hooks::start_hook_listener → event_normalizer::normalize → app.emit("agent:event", event) → useAgentEventsStore.addEvent(event)CLI listener
Section titled “CLI listener”Purpose: Receives commands from the magia CLI binary and from scripts that want to control the app programmatically (e.g. open a project, create a session).
Protocol: Line-delimited JSON commands over the CLI socket.
Permission listener
Section titled “Permission listener”Purpose: Intercepts tool permission requests from agent processes before they are executed.
Protocol: The agent writes a permission request to the permission socket. The Rust listener holds the request and emits a Tauri event to the frontend. The frontend renders the permission dialog; the user approves or denies; the frontend calls an invoke() command to respond; the Rust listener unblocks the agent.
Agent (awaiting permission) → Unix socket (permission path) → agent::start_permission_listener → app.emit("agent:permission-request", request) → Frontend PermissionDialog → invoke("respond_to_permission", { id, approved }) → agent::respond_to_permission → unblocks agentOTel gRPC collector
Section titled “OTel gRPC collector”Purpose: Receives OpenTelemetry spans and metrics from agent CLIs that export telemetry data.
Protocol: gRPC (tonic) over a Unix socket, with automatic fallback to HTTP/TCP on a configurable port (default 4318). Metrics are stored in SharedMetricsStore and forwarded to the frontend via otel:metrics-update events.
WebSocket IDE server
Section titled “WebSocket IDE server”Purpose: Allows IDE extensions (e.g. the Claude Code VS Code extension) to connect to Magia and exchange session events in real time.
Protocol: WebSocket on a dynamically-assigned TCP port. The port is stored in IdeServer managed state and can be queried via invoke("get_ide_server_port"). The protocol mirrors the Claude Code IDE integration protocol.
VS Code extension → WebSocket (localhost:{port}) → ide_server::IdeServer → session events forwarded bidirectionallyIPC transport abstraction
Section titled “IPC transport abstraction”The ipc module (src-tauri/src/ipc.rs) wraps the interprocess crate to provide a single API that works on all platforms:
- macOS / Linux: file-based Unix domain sockets.
- Windows: named pipes (
\\.\pipe\magia-{name}).
All socket creation in the hook, CLI, permission, and OTel listeners goes through ipc::socket_name() so the rest of the codebase stays platform-agnostic.