In the previous chapter, Unified AI Agent & LLM Providers, we gave Jarvis a brain. It can think, make decisions, and use tools.
However, we have a disconnect. The "Brain" (AI Logic) lives in a background Node.js process, but the "Face" (The Dashboard, Settings, and Popups) lives in a separate React window.
If the Brain decides to open an app, the Face doesn't know about it. If you change a setting in the Face, the Brain doesn't know the rule changed.
In this chapter, we will build the Nervous System that connects these two parts using IPC (Inter-Process Communication).
Imagine a busy restaurant:
The Problem: For safety reasons, customers are not allowed to walk into the kitchen to grab food, and chefs don't come out to tables to cook.
The Solution: You need a Waiter (IPC).
In Electron, this "Waiter" system is how we keep the UI and the Logic in sync.
preload.ts)This is the Service Window. It is a special security bridge. It is the only place where we allow the UI to send specific messages to the Main process.
Think of these as "Subject Lines" in an email.
invoke / handle: "Please get me the data" (Request/Response).send / on: "Hey, just letting you know..." (One-way notification).Let's visualize the flow when the Dashboard asks for User Statistics.
We will implement this "Waiter" system in three steps: The Kitchen (Handler), The Bridge (Preload), and The Customer (UI).
First, we define what the backend can do. We use ipcMain.handle to listen for requests.
We organize all our handlers in src/ipc/ipc-handlers.ts.
// src/ipc/ipc-handlers.ts
import { ipcMain } from 'electron';
// This is the Chef waiting for an order
ipcMain.handle('get-stats', async () => {
console.log('Kitchen: Receiving order for stats...');
// 1. Get the data from our analytics manager
const stats = await analyticsManager.getStats();
// 2. Send it back to the waiter
return stats;
});
Now we need to expose this capability to the frontend safely. We use contextBridge in src/preload.ts.
// src/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// We expose a secure API called 'electronAPI'
contextBridge.exposeInMainWorld('electronAPI', {
// The UI calls this function
getStats: () => {
// The bridge passes the message to the Main Process
return ipcRenderer.invoke('get-stats');
}
});
Beginner Note: We never expose ipcRenderer directly to the UI. We only expose specific functions (getStats). This prevents a malicious website from taking over your computer.
Finally, in our React component (src/components/Dashboard.tsx), we call this function just like any other API.
// src/components/Dashboard.tsx
const Dashboard = () => {
const [stats, setStats] = useState(null);
useEffect(() => {
const fetchData = async () => {
// Call the bridge!
// 'window.electronAPI' exists because of the preload script
const data = await window.electronAPI.getStats();
setStats(data);
};
fetchData();
}, []);
if (!stats) return <div>Loading...</div>;
return <div>Total Words Spoken: {stats.totalWords}</div>;
};
Sometimes, the Kitchen needs to tell the Customer something without being asked (e.g., "Your table is ready!" or "Transcription Complete!").
For this, we use a Listener.
When a transcription finishes in src/main.ts (or the transcription service):
// src/main.ts
// Send a message to the specific window
mainWindow.webContents.send('transcription-complete', 'Hello World');
We update src/preload.ts to allow the UI to register a callback function.
// src/preload.ts
contextBridge.exposeInMainWorld('electronAPI', {
// ... previous code ...
// Allow UI to provide a function to run when data arrives
onTranscription: (callback) => {
ipcRenderer.on('transcription-complete', (event, text) => {
callback(text);
});
}
});
In src/App.tsx, we set up the listener.
// src/App.tsx
useEffect(() => {
// Define what to do when the message arrives
window.electronAPI.onTranscription((text) => {
console.log("New text arrived:", text);
showNotification(text);
});
// Cleanup: In a real app, we would remove the listener here
}, []);
A common bug in these apps is when the UI thinks one thing (e.g., "I am recording") but the Backend thinks another (e.g., "I am idle").
To solve this, we treat the Main Process as the Source of Truth. The UI is just a reflection of the Main Process.
In src/App.tsx, we see this pattern used for Onboarding:
// src/App.tsx (Simplified)
useEffect(() => {
const checkStatus = async () => {
// 1. Ask Main Process: "Is the user new?"
const isCompleted = await window.electronAPI.checkOnboardingStatus();
// 2. Update React State based ONLY on Main Process answer
setHasCompletedOnboarding(isCompleted);
};
checkStatus();
}, [user]);
By forcing the UI to ask the Backend, we ensure that if you restart the app, the state remains consistent.
In this chapter, we built the IPC & State Management system:
ipcMain.handle in the backend to answer requests.contextBridge in the Preload Script to safely expose these features.Now Jarvis is a complete desktop application. It listens, thinks, acts, and updates the user interface seamlessly.
But what happens when you step away from your computer? You can't carry your desktop with you. To make Jarvis truly ubiquitous, we need to extend it to your phone.
In the final chapter, we will look at how we connect a mobile companion app to this system.
Next Chapter: iOS Companion Architecture
Generated by Code IQ