Skip to content

Latest commit

 

History

History
175 lines (130 loc) · 8.87 KB

File metadata and controls

175 lines (130 loc) · 8.87 KB

Kai: Electron Desktop AI Assistant

What Is This?

An Electron desktop app that provides a local-first AI chat experience with tool use, MCP integration, skills, sub-agents, and memory. Built with React 19 + TypeScript + Tailwind CSS 4, orchestrated by Mastra.

Author: Contributed by community member License: MIT Package Manager: pnpm 10+ Node: 22+

Commands

pnpm install          # install dependencies
pnpm dev              # start electron in dev/watch mode
pnpm build            # build main + preload + renderer
pnpm build:mac        # build + package for macOS (arm64 + x64)
pnpm lint             # eslint (ts/tsx only)
pnpm type-check       # tsc --noEmit
pnpm test             # vitest run (unit + component tests)
pnpm test:watch       # vitest in watch mode
pnpm preview          # preview production build
pnpm rebuild          # rebuild native deps (electron-builder install-app-deps)

Architecture

Three Electron process layers with strict isolation:

electron/main.ts          <- Main process: window, menus, IPC registration, tool init
electron/preload.ts       <- Preload: exposes `window.app` API via contextBridge
src/App.tsx               <- Renderer: React shell, sidebar, conversations, settings

Main Process (electron/)

Directory Purpose
electron/agent/ Mastra agent orchestration, model catalog, memory, sub-agents, tokenization
electron/ipc/ IPC handler registration (agent, config, conversations, mcp, memory, oauth, skills)
electron/tools/ Tool implementations + registry builder
electron/config/schema.ts Zod config schema (AppConfig) - central to everything
electron/main.ts App bootstrap, window creation, menu, hot-reload for MCP + skills

Renderer (src/)

Directory Purpose
src/components/thread/ Chat thread, composer, markdown, code blocks, tool groups, sub-agent views
src/components/settings/ Settings panels (models, tools, MCP, memory, compaction, skills, advanced)
src/components/conversations/ Sidebar conversation list, sub-agent section
src/components/agent-lattice/ Agent Lattice OAuth auth banner
src/providers/ React context providers (Config, Runtime, Attachments)
src/lib/ IPC client wrapper, utilities

Key Files

  • electron/config/schema.ts - Zod schema defining all config (AppConfig type)
  • electron/tools/registry.ts - Builds active tool set from config, skills, MCP servers
  • electron/preload.ts - The window.app IPC bridge (renderer's only way to talk to main)
  • electron/agent/mastra-agent.ts - Mastra agent setup and streaming
  • src/providers/RuntimeProvider.tsx - Manages conversation streaming state in renderer
  • src/providers/ConfigProvider.tsx - Reads/writes config via IPC
  • electron-builder.yml - macOS packaging config
  • electron.vite.config.ts - Vite config for main, preload, and renderer builds

Config

All app state lives under ~/.kai/:

Path Contents
~/.kai/config.json Primary config (models, tools, MCP, memory, compaction, etc.)
~/.kai/data/ Conversation persistence
~/.kai/skills/ Installed skill directories
~/.kai/certs/ TLS certificates for integrations
~/.kai/settings/llm.json Imported provider/model settings

Config changes trigger hot-reload for MCP servers and skills (fingerprint diffing in main.ts).

Code Style

ESLint enforces (see eslint.config.js):

  • consistent-type-imports (error) - use import type for type-only imports
  • no-explicit-any (warn)
  • no-unused-vars (warn, _ prefix ignored)
  • no-console (warn, console.warn/error/info allowed)

Additional conventions:

  • Tailwind CSS 4 (PostCSS plugin, not the old tailwind.config.js approach)
  • Radix UI primitives for all interactive components
  • @/ path alias maps to src/
  • Lucide React for icons

IPC Boundary

Renderer code never accesses Node APIs directly. All communication goes through window.app (defined in preload.ts):

  • window.app.agent.* - streaming, title generation, sub-agents
  • window.app.config.* - get/set config, change listeners
  • window.app.conversations.* - CRUD, active conversation tracking
  • window.app.mcp.* - test MCP connections
  • window.app.memory.* - clear memory stores
  • window.app.skills.* - list/get/delete/toggle skills
  • window.app.dialog.* - native file picker
  • window.app.image.* - fetch/save images (bypasses CORS)

Model Providers

Supported via Mastra + AI SDK:

  • OpenAI-compatible (custom endpoints)
  • Anthropic
  • Amazon Bedrock (AWS credential chain)
  • Google

Config schema: models.providers (keyed by name) + models.catalog (array of model entries).

Gotchas

  • macOS only for now - electron-builder.yml only targets mac with dmg output
  • contextIsolation is ON - never try to use require() or Node APIs in renderer code
  • Tailwind v4 - uses @tailwindcss/postcss plugin, not the v3 tailwind.config.js pattern
  • Tests - Vitest is the test runner (pnpm test); the suite runs on every PR and on pre-push via Husky
  • sandbox: false in webPreferences - needed for preload script to work with full Node access
  • No conversation auto-cleanup - conversations are never auto-deleted; users manage their own threads
  • Tool registry is async - tools build after window creation; MCP connections happen at startup
  • Config persistence allowlist - desktopConfigPayload() in electron/ipc/config.ts is an explicit allowlist; new config sections MUST be added there or they won't persist

Testing Conventions

Tests follow a small set of conventions documented in CONTRIBUTING.md: explicit assertions only (no snapshots), helper-driven fixtures from test-utils/ over inline setup, no real network egress (the fetch firewall in vitest.setup.ts fails-closed against unmocked provider hosts), and reliance on the global determinism seams — frozen system time, deterministic UUIDs, and the @lydell/node-pty stub. For the architectural rationale behind these choices, see docs/TESTING_ARCHITECTURE.md.

Debug Logging

When debugging issues that span process boundaries (renderer <-> main) or involve async race conditions, use the project-local debug log directory instead of console.warn:

Item Value
Directory debug-logs/ (project root, gitignored)
Convention One file per topic, e.g. debug-logs/persist.log, debug-logs/stream.log
Format [ISO-timestamp] [TAG] key=value key=value ... — structured, grep-friendly

How to use

Main process (Node / Electron main) — write directly with fs.appendFileSync:

import { appendFileSync } from 'fs';
import { join } from 'path';
const DEBUG_LOG = join(__dirname, '../../debug-logs/persist.log');
function debugLog(msg: string) {
  try {
    appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`);
  } catch {}
}

Renderer process — use console.warn with a [TAG] prefix (renderer cannot write to disk). The user will relay console output, or you can read it from DevTools.

When the user says "enable debug mode" or "add debug logs"

  1. Create the log file(s) in debug-logs/.
  2. Add appendFileSync-based logging in the main process code at the points of interest.
  3. Add console.warn-based logging in the renderer at the points of interest.
  4. Ask the user to reproduce, then read debug-logs/*.log to diagnose.
  5. Clean up: remove the debug logging code once the issue is resolved. Debug logs are for investigation, not permanent instrumentation.

Last Updated: 2026-04-22