Floating circular AI assistant widget for macOS. Built on Friday Core (the stable Tauri v2 runtime) with a Project Friday phase system that lets you extend Veronica's capabilities without ever touching the core engine.
Iron Man arc-reactor aesthetic. Local-first. Always on top. Always watching.
- Holographic widget — radial filaments + sphere wireframe + pulsing core, drawn on Canvas 2D
- Click → conversation — input dialog → LLM → spoken reply (or mic if voice mode enabled)
- Voice output — Lemonfox TTS (heart voice) with macOS
sayfallback - Multi-turn memory — keeps last 12 conversation turns in context
- Tool calling — AI can:
- read/write files, list directories
- run shell commands
- open macOS apps
- search the web (DuckDuckGo)
- get current time, battery, calendar events
- control system volume
- Smart click-through — clicks outside the circle pass to the app below; only the circle itself is interactive
- Position lock (⌘⇧L), tray menu, persistent position
- Phase system — Project Friday phases plug in at boot via
registerPhase(), isolated from the core
┌─────────────────────────────────────────────────────┐
│ Veronica — Floating widget (140×140 transparent) │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Friday Core (stable runtime, never broken)│ │
│ │ • renderer.js — Canvas 2D hologram │ │
│ │ • state-machine.js — 5 visual states │ │
│ │ • interactions.js — click/drag/menu │ │
│ │ • actions.js — extension scaffolding │ │
│ └───────────────────────┬─────────────────────┘ │
│ │ VeronicaAPI │
│ ┌───────────────────────▼─────────────────────┐ │
│ │ Veronica Layer (src/veronica/) │ │
│ │ • veronica-api.js — stable phase contract│ │
│ │ • phase-registry.js — phase lifecycle │ │
│ └───────────────────────┬─────────────────────┘ │
│ │ init(api) │
│ ┌───────────────────────▼─────────────────────┐ │
│ │ Project Friday Phases (pluggable) │ │
│ │ phase-a, phase-b, phase-c … │ │
│ │ Each isolated — one crash ≠ others crash │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────┬─────────────────────────────────┘
│ tauri invoke
┌───────────────────▼─────────────────────────────────┐
│ Rust backend (src-tauri/src/main.rs) │
│ • passthrough watcher (CoreGraphics) │
│ • chat: Ollama tool-loop with memory │
│ • voice: sox → Lemonfox STT │
│ • speak: Lemonfox TTS → afplay │
│ • tools: file / shell / web / sys │
│ • tray + global shortcut + lock │
└─────────────────────────────────────────────────────┘
Key design rule: Project Friday phases import from src/veronica/veronica-api.js only — never from Friday Core modules directly. This means:
- Friday Core internals can be refactored without breaking phases
- New phases in Project Friday don't impact or modify the core runtime
- A failing phase is caught and logged; it cannot crash other phases or the widget
| Layer | Technology |
|---|---|
| Desktop shell | Tauri v2 (Rust) — frameless transparent always-on-top window, ~9 MB binary |
| UI renderer | HTML/Canvas/JS — Friday Core visual engine + state machine |
| Phase system | ES module plugins via VeronicaAPI |
| LLM | Ollama (local) — qwen2.5:7b default; full tool calling |
| Voice input | Lemonfox STT + sox |
| Voice output | Lemonfox TTS (heart voice) with macOS say fallback |
| Click-through | CoreGraphics — global cursor polling for circle hit-testing |
# Toolchain
brew install node
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install sox # only needed for voice input
# Local LLM
brew install ollama
ollama pull qwen2.5:7b
ollama serve # leave runningKeys live in ~/.friday/, never in the repo.
mkdir -p ~/.friday && chmod 700 ~/.friday
# Lemonfox (TTS heart voice + STT) — recommended
echo 'YOUR_LEMONFOX_KEY' > ~/.friday/lemonfox-key && chmod 600 ~/.friday/lemonfox-key
# Anthropic Claude direct (optional fallback)
echo 'sk-ant-...' > ~/.friday/anthropic-key && chmod 600 ~/.friday/anthropic-key
# AWS Bedrock long-term key (optional fallback)
echo 'BedrockAPIKey-...' > ~/.friday/bedrock-key && chmod 600 ~/.friday/bedrock-keyLLM order of preference: Ollama (local) → Anthropic → Bedrock.
git clone https://github.com/Srimi1/Veronica.git
cd Veronica
npm install
npm run tauri build
cp -R "src-tauri/target/release/bundle/macos/Veronica.app" /Applications/
xattr -dr com.apple.quarantine "/Applications/Veronica.app"
open -a "Veronica"npm install
npm run tauri dev- Click the widget circle → input dialog → type your request → reply is spoken
- Drag to reposition (works anywhere when unlocked)
- ⌘⇧L → toggle position lock
- Tray icon → Lock / Reset / Quit
- Right-click widget → state menu + phase debug
- Keys 1–5 → force state (dev mode)
Position is saved to ~/Library/Application Support/com.veronica.app/position.json.
| Tool | Purpose |
|---|---|
get_time |
Current local date/time |
read_file, write_file, list_dir |
Filesystem |
run_shell |
Execute zsh commands |
open_app |
Launch a macOS app |
web_search |
DuckDuckGo search |
get_battery |
Mac battery + charging state |
calendar_today |
Today's Calendar.app events |
set_volume |
System output volume 0–100 |
Try: "what's on my calendar", "open Safari", "set volume to 30", "search latest M4 Mac mini reviews"
A phase is a plain JS object with a name, version, init(api), and optional destroy().
// src/phases/my-phase.js
import { registerPhase } from '../veronica/phase-registry.js';
registerPhase({
name: 'my-phase',
version: '1.0.0',
async init(api) {
// api is a VeronicaAPI instance — the ONLY thing phases touch.
// Never import renderer.js, state-machine.js, etc. directly.
// React to state changes
const unsub = api.onStateChange((state) => {
console.log('[my-phase] state changed to', state);
});
// Drive state transitions
api.setState('LISTENING');
// Call Rust backend commands
const locked = await api.invoke('is_locked');
// Inter-phase events
api.emit('my-phase:ready', { version: '1.0.0' });
await api.listen('other-phase:data', ({ payload }) => {
console.log('[my-phase] received', payload);
});
// Store cleanup ref
this._unsub = unsub;
},
destroy() {
this._unsub?.();
}
});Then import your phase file before main.js runs (or dynamically via window.VERONICA.registerPhase()).
| Property | Required | Description |
|---|---|---|
name |
✅ | Unique string identifier |
version |
recommended | Semver string |
init(api) |
recommended | Called once at boot with VeronicaAPI |
destroy() |
optional | Called on unload or explicit disable |
api.setState(state) // transition: IDLE/LISTENING/THINKING/SPEAKING/ALERT
api.getState() // → current state string
api.onStateChange(fn) // subscribe → returns unsubscribe fn
api.setAlert(message) // trigger ALERT with message
api.invoke(cmd, args) // call a Tauri Rust command
api.listen(event, handler) // subscribe to Tauri or inter-phase events
api.emit(event, payload) // broadcast inter-phase event
api.rendererState // read-only renderer snapshot
api.version // Veronica API version stringPhase A fails init()
└── error caught, logged as PhaseStatus.ERROR
└── Phase B, Phase C, Friday Core — all unaffected ✓
Every phase's init() and destroy() runs inside a try/catch. A broken phase is quarantined — it never propagates exceptions to other phases or to the widget runtime.
Environment variables (optional):
| Var | Default | Purpose |
|---|---|---|
FRIDAY_LLM_MODEL |
qwen2.5:7b |
Ollama model name |
FRIDAY_TTS_VOICE |
heart |
Lemonfox voice |
FRIDAY_VOICE_MODE |
0 |
1 = mic input on click, 0 = typed dialog |
Open DevTools in Tauri dev mode:
// Check phases
window.VERONICA.phases()
// → [{ name, version, status, error }]
// Drive state from console
window.VERONICA.api.setState('ALERT')
// Register a phase at runtime
window.VERONICA.registerPhase({ name: 'test', init(api) { console.log(api.version) } })
// Friday Core internals (backwards compat)
window.FRIDAY.stateMachine.state
window.FRIDAY.rendererVeronica/
├── src/
│ ├── main.js # Entry point — boots Friday Core + Veronica layer
│ ├── renderer.js # Canvas 2D hologram (filaments, sphere, particles)
│ ├── state-machine.js # 5-state lifecycle
│ ├── interactions.js # Click, drag, context menu
│ ├── actions.js # Extension scaffolding
│ ├── styles.css # Transparent widget CSS
│ ├── index.html # Widget shell
│ └── veronica/
│ ├── veronica-api.js # Stable API contract for phases
│ └── phase-registry.js # Phase lifecycle manager
├── src-tauri/
│ ├── src/main.rs # Rust backend (chat, tools, tray, position)
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ └── capabilities/
│ └── default.json
├── package.json
└── vite.config.js
MIT