Both SDKs are installed and demoed on their own screens, so you can see a working call before you write one.
Three screens are relevant: AI holds the API key panel, Demo shows a streaming chat, and Claude runs the Claude Agent SDK inside the app.
Where the key lives
API keys go through the same encrypted store as license keys, so they are protected by the operating system and never written in plaintext.
The rule that matters: the key stays in the main process. Anything in the renderer ships inside your app bundle, where a curious user can read it in about a minute. The AI calls happen in src/main/ai/, and the renderer only ever sees the text coming back.
Streaming
Streaming is already parsed for you. In a component:
const { stream, isStreaming } = useAi('claude');
async function ask() {
for await (const chunk of stream('Summarise this file in five bullets')) {
setOutput((prev) => prev + chunk);
}
}The main process handles the server-sent events, the backpressure, and the cleanup when a window closes mid-response.
Adding your own provider
Three files, the same pattern as everything else:
src/main/ai/<provider>.ts: the SDK client plus a generator that yields textsrc/shared/ipc/ai-channels.ts: add the channel namesrc/renderer/hooks/useAi.ts: add the provider to the union type
Two things worth knowing
Pin your model, and expect to change it
The demo uses a current Claude model. Model identifiers get deprecated. Put the identifier in one constant so a bump is a one-line change rather than a search across your codebase.
Users pay for tokens too
A desktop app that calls an LLM with the user's own key has a cost the user can see. Show token or request counts somewhere. Users forgive cost; they do not forgive surprise.
In this Electron template, add a feature that takes the text file the user picked on the FileStats screen and asks Claude to summarise it. Follow the pattern in src/features/ai-demo/: the SDK call goes in the main process, the renderer gets a stream. Do not put the API key anywhere the renderer can reach.