diff --git a/.gitignore b/.gitignore
index 57cbcfa..a7a5e72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules/
+.opencluely-cache/
.env
.venv-whisper/
.whisper-models/
@@ -7,3 +8,11 @@ dist/
bin/
.DS_Store
*.log
+
+# Local coaching materials (not part of the application release)
+/MISSION.md
+/RESOURCES.md
+/learning-records/
+/lessons/
+/reference/
+/assets/course.css
diff --git a/README.md b/README.md
index a2ed045..6e9dcee 100644
--- a/README.md
+++ b/README.md
@@ -1,237 +1,191 @@
-# OpenCluely
+# OpenCluely — Gemini Live Fork
-**The invisible AI interview copilot.**
+**A stateful, real-time screen and conversation copilot for practice and AI-permitted calls.**
-Real-time AI help on a stealth overlay that screen sharing cannot see. Ask by voice or screenshot, and get clear answers that stream in as you need them.
-
-
-
-
-
-
-
-
-
Website |
-
Download |
-
Quick start |
-
How it works
+[What changed](#what-this-fork-changes) · [Quick start](#quick-start) · [Controls](#controls) · [Browser rail](#browser-rail) · [Upstream](https://github.com/TechyCSR/OpenCluely)
-## Demo
-
-https://github.com/user-attachments/assets/896a7140-1e85-405d-bfbe-e05c9f3a816b
-
-## What it is
-
-OpenCluely is a desktop app for technical interviews and practice. It places a small overlay on your screen that recording and conferencing tools do not capture. You can speak a question or take a screenshot, and the AI answers in real time. The answer streams into a floating window and an optional chat panel, with clean code blocks and syntax highlighting.
-
-It is free and open source. Processing stays on your machine, and the only thing that leaves your device is the request you send to the AI provider.
-
-## Highlights
-
-- **Invisible overlay.** Windows stay out of Zoom, Google Meet, Microsoft Teams, Discord, and OBS captures. You see the answer, the call does not.
-- **Hidden during screen share.** When a share starts, the app can hide every window on its own.
-- **Flexible local voice.** Choose manual start/stop capture or automatic voice-activity detection without fixed-timer sentence cuts.
-- **Configurable streamed answers.** Route voice replies to chat, the floating overlay, or both.
-- **Direct image analysis.** Screenshots go straight to Gemini for visual reasoning, with no slow OCR step in between.
-- **Session memory.** The whole conversation is remembered, so follow-ups, edge cases, and optimizations keep their context.
-- **Language aware.** Tailored answers for C++, C, Python, Java, and JavaScript.
-- **Stealthy by design.** Runs under ordinary system names, ships with no telemetry, and keeps your session local.
-- **Cross platform.** Pre-built installers for Windows and Linux (.deb and AppImage). macOS runs from source in one command.
-
-## Download
-
-Pre-built installers are published with every release. These links always point at the newest version.
+> This fork explores a Gemini Live-first architecture on macOS. It is based on [TechyCSR/OpenCluely](https://github.com/TechyCSR/OpenCluely) and retains its Apache 2.0 license and attribution.
+
+## What this fork changes
+
+The original app is centered on one-shot screenshots and optional speech providers. This fork adds a persistent assistance loop that combines changing screen context with live conversation while preserving the user's active objective.
+
+- **Gemini Live audio.** Streams macOS system audio to Vertex AI Gemini Live for low-latency transcription and responses.
+- **Stateful screen understanding.** Samples the active screen continuously, suppresses duplicate frames, and maintains an active-task anchor.
+- **Intent-aware assistance.** Short conversational detours do not erase the current coding problem, document, or unfinished step.
+- **Useful technical structure.** Coding responses prioritize intent, approach, correctness, walkthrough, code, complexity, and edge cases.
+- **Multiple response surfaces.** Results can appear in the native answer overlay, chat window, or an explicitly activated browser rail.
+- **Focus-safe macOS overlays.** Operational overlay windows avoid activating the app or pulling focus away from the current application.
+- **Reduced local load.** The packaged runtime no longer ships the legacy Whisper worker, models, or heavy speech dependencies.
+- **No API-key fallback.** The Gemini path uses Vertex AI with local Google Application Default Credentials.
+
+## Architecture
+
+```mermaid
+flowchart LR
+ A["macOS system audio"] --> B["PCM normalization and mixing"]
+ C["Live screen frames"] --> D["Frame deduplication and task state"]
+ B --> E["Vertex AI Gemini Live"]
+ D --> E
+ E --> F["Stateful response stream"]
+ F --> G["Native answer overlay"]
+ F --> H["Chat window"]
+ F --> I["Local browser bridge"]
+ I --> J["User-activated browser rail"]
+```
-| Platform | File | Notes |
-|---|---|---|
-| Windows | [Setup .exe](https://github.com/TechyCSR/OpenCluely/releases/latest) | NSIS installer. Adds a Start Menu shortcut. |
-| Linux (Debian or Ubuntu) | [.deb](https://github.com/TechyCSR/OpenCluely/releases/latest) | Pulls system deps automatically (Python, ffmpeg, GTK). |
-| Linux (universal) | [.AppImage](https://github.com/TechyCSR/OpenCluely/releases/latest) | No install. Run `chmod +x` then launch. |
+The browser bridge binds only to `127.0.0.1`. It accepts Chrome-extension origins and a small allowlist of commands and response channels. Ordinary webpages cannot connect directly.
-> **macOS:** there is no pre-built download. The app is unsigned and un-notarized, so macOS Gatekeeper blocks it as "damaged and can't be opened." Run OpenCluely from source instead — see [Quick start](#quick-start). It is a one-line `./setup.sh` once Node.js is installed.
+## Requirements
-Every build is produced automatically on GitHub Actions and ships with SHA-256 checksums. Each release also lists the full set of commits it includes.
+The new live-audio path is currently macOS-focused.
-The website at [opencluely.techycsr.dev](https://opencluely.techycsr.dev) detects your operating system and offers the right installer directly.
+- macOS with Screen Recording permission
+- Node.js 18 or newer
+- Google Cloud CLI (`gcloud`)
+- A billed Google Cloud project with Vertex AI enabled
+- `ffmpeg` available on `PATH`
+- Chrome only if you want the optional browser rail
## Quick start
-If you would rather build from source, three steps are all it takes.
-
-1. Clone the repository.
+1. Clone this fork.
```bash
- git clone https://github.com/TechyCSR/OpenCluely.git
+ git clone https://github.com/0xtigerclaw/OpenCluely.git
cd OpenCluely
```
-2. Run the setup script.
+2. Install dependencies and create the local environment file.
```bash
- ./setup.sh
+ ./setup.sh --no-run
```
- The script installs Node dependencies, creates your `.env` from the example, sets up a local Whisper virtual environment, points the config at it, and launches the app.
+3. Authenticate the Google account that has access to your Vertex AI project.
-3. Add your Gemini key.
+ ```bash
+ gcloud auth application-default login
+ ```
- On first launch the Settings window opens automatically. Get a free key from [Google AI Studio](https://aistudio.google.com/) and paste it in, or edit `.env` directly. Both work, and changes are picked up without a restart.
+4. Add your project to `.env`.
-### Platform notes
+ ```bash
+ GOOGLE_CLOUD_PROJECT=your-billed-gcp-project
+ GOOGLE_CLOUD_LOCATION=europe-west4
+ GEMINI_LIVE_MODEL=gemini-live-2.5-flash-native-audio
+ FFMPEG_COMMAND=ffmpeg
+ ```
-- On Windows, use Git Bash (included with Git for Windows) or WSL to run `setup.sh`.
-- On macOS and Linux, your normal terminal works.
-- **macOS users must build from source** (steps above) — there is no pre-built `.dmg`. Because the app is unsigned, a downloaded build would be blocked by Gatekeeper as "damaged"; running from source avoids that entirely.
-- No manual `npm` commands are needed. The script handles everything.
+5. Start the app.
-### Setup script options
+ ```bash
+ npm start
+ ```
-```bash
-./setup.sh --build # Build a distributable for your OS
-./setup.sh --ci # Use npm ci instead of npm install
-./setup.sh --no-run # Set up only, do not launch
-./setup.sh --install-system-deps # Install sox for the microphone (optional)
-./setup.sh --skip-whisper # Skip the local Whisper bootstrap
-```
+On first use, grant Screen Recording permission under **System Settings → Privacy & Security → Screen & System Audio Recording** and relaunch OpenCluely.
-## Configuration
+## Google Cloud setup
-The setup script writes sensible defaults. The only required value is a Gemini API key.
+Your project must have billing and Vertex AI access. If you have permission to enable services:
```bash
-# Required
-GEMINI_API_KEY=your_gemini_api_key_here
-
-# Optional speech provider. Pick one.
-SPEECH_PROVIDER=whisper
-
-# Azure option
-AZURE_SPEECH_KEY=your_azure_speech_key
-AZURE_SPEECH_REGION=your_region
-
-# Local Whisper option
-WHISPER_COMMAND=whisper
-WHISPER_MODEL_DIR=.whisper-models
-WHISPER_MODEL=small
-WHISPER_LANGUAGE=auto
-WHISPER_DEVICE=auto
-WHISPER_PYTHON=
-WHISPER_CAPTURE_MODE=vad
-WHISPER_RESPONSE_TARGET=both
-WHISPER_MANUAL_MAX_MS=90000
-WHISPER_GPU_IDLE_MS=60000
+gcloud services enable aiplatform.googleapis.com --project your-billed-gcp-project
```
-Speech is optional. If no provider is configured, the microphone button hides itself across the app.
-
-## Optional voice setup
-
-You can use local Whisper for offline transcription or Azure Speech for a cloud option.
-
-For local Whisper, `./setup.sh` handles the full setup. It creates `.venv-whisper`, installs `openai-whisper`, points `.env` at the virtual environment, creates `.whisper-models`, and runs a quick speech test. The app reads its own PCM WAV recordings directly; ffmpeg is only needed when transcribing other audio formats through the CLI fallback.
-
-For Azure Speech, create a Speech resource in the [Azure Portal](https://portal.azure.com/), then add the key and region to `.env` with `SPEECH_PROVIDER=azure`.
-
-## How it works
-
-1. **Ask.** Use automatic pause detection, choose manual start/stop capture in Settings, or use the screenshot shortcut.
-2. **Reason.** Gemini reads the audio or image with full conversation context and works toward a precise answer.
-3. **Answer.** Voice responses stream to chat, the overlay, or both, according to Settings.
-
-## Keyboard shortcuts
+Application Default Credentials remain in your normal local Google Cloud configuration. They are never committed to this repository. To switch accounts, rerun `gcloud auth application-default login` and select the intended account.
-| Action | Shortcut | Description |
-|---|---|---|
-| Screenshot capture | `Cmd/Ctrl + Shift + S` | Capture the screen and analyze it with Gemini |
-| Toggle speech | `Alt + R` | Start or stop voice recognition, if configured |
-| Toggle visibility | `Cmd/Ctrl + Shift + V` | Show or hide all windows |
-| Toggle interaction | `Cmd/Ctrl + Shift + I` or `Alt + A` | Enable or disable click through |
-| Open chat | `Cmd/Ctrl + Shift + C` | Open the interactive chat window |
-| Settings | `Cmd/Ctrl + ,` | Open the settings panel |
+## Controls
-## Project status
+| Action | macOS shortcut | Purpose |
+| --- | --- | --- |
+| Toggle Live Screen | `Cmd + Shift + G` | Continuously analyze meaningful screen changes |
+| Toggle Call Copilot | `Cmd + Shift + L` | Start or stop system-audio streaming to Gemini Live |
+| Analyze one screenshot | `Cmd + Shift + S` | Force a fresh one-shot screen analysis |
+| Open chat | `Cmd + Shift + C` | Ask a typed follow-up with session context |
+| Bring windows forward | `Cmd + Shift + V` | Show OpenCluely windows on the current desktop |
+| Toggle interaction | `Cmd + Shift + I` or `Option + A` | Enable or disable clicks on native overlays |
+| Clear task memory | `Cmd + Shift + \` | Start a clean task or conversation |
+| Open settings | `Cmd + ,` | Configure the app |
+| Force always-on-top | `Cmd + Shift + T` | Restore overlay window level |
-OpenCluely is under active development. The core is stable and improvements ship regularly.
+Use the **Exit App** button in the UI or the application menu to stop capture and close all processes cleanly.
-### Done
+## Browser rail
-- Stealth overlay with a draggable command bar and a click through toggle
-- Hidden during screen share, with automatic hiding when a share begins
-- Screenshot capture with direct Gemini analysis, no OCR step
-- Configurable manual or VAD-driven voice capture
-- Persistent local Whisper worker with optional CUDA acceleration and idle GPU release
-- Configurable chat/overlay routing for streamed voice answers
-- Whisper hallucination filter that drops phantom phrases on silence
-- AI response window with markdown and syntax highlighting
-- Global shortcuts for capture, visibility, interaction, chat, and settings
-- Session memory and a full chat UI
-- Language picker and a DSA skill prompt
-- Optional Azure Speech and local Whisper, with an auto hiding mic button
-- Multi-monitor and area capture support
-- Window binding and positioning
-- Settings management with disguise and stealth modes
+The optional Chrome extension displays OpenCluely responses inside only the tab you explicitly activate.
-### Planned
+1. Open `chrome://extensions`.
+2. Enable **Developer mode**.
+3. Choose **Load unpacked**.
+4. Select this repository's `browser-extension` directory.
+5. Pin **OpenCluely Browser Rail** from Chrome's extensions menu.
+6. On the page where you want the rail, click its toolbar icon.
-- Multiple model backends alongside Gemini (OpenAI, Anthropic, local)
-- Auto typing of code snippets into editors and IDEs
-- Export of conversation history to markdown or PDF
-- Deeper stealth, including process name randomization
+The extension uses `activeTab` and `scripting`; it does not request persistent access to every website. Activate it again after navigating to a new page.
-## Troubleshooting
+> The browser rail is rendered inside the webpage. It is visible in tab, window, and full-screen sharing. Use the native overlay or a separate unshared display when the rail should not be included in a permitted screen share.
-
-Setup issues
+## How state is handled
-- **setup.sh will not run.** Make sure you are in the project folder (`cd OpenCluely`) and that the script is executable (`chmod +x setup.sh`). On Windows, use Git Bash.
-- **Setup stops with exit code 130.** That means Ctrl+C was pressed. Run `./setup.sh` again.
-- **Node or npm not found.** Install Node.js 18 or newer from [nodejs.org](https://nodejs.org/), restart the terminal, and retry.
+OpenCluely keeps one active-task anchor and treats brief unrelated conversation as a temporary detour. For example, a social question during a coding exercise can receive a short answer without discarding the problem statement, current approach, code, or unfinished step. An explicit task switch—or sustained work on a new task—replaces the anchor.
-
+Screen frames refine that state instead of creating a new conversation on every capture. Duplicate or low-information frames are skipped to reduce cost, latency, and answer churn.
-
-App issues
-
-- **Electron will not start or shows a blank window on Linux.** Try `npm run dev`, and make sure X11 or XWayland is available in headless setups.
-- **macOS screen capture does not work.** Grant Screen Recording permission under System Settings, Privacy and Security, then relaunch the app.
-- **Windows SmartScreen blocks the app.** Click More info, then Run anyway, or use `npm start` during development.
-- **Microphone or voice not working.** Voice is optional. For Azure, add valid keys to `.env`. For Whisper, install `openai-whisper`, `ffmpeg`, and `sox`, then set `SPEECH_PROVIDER=whisper`.
-
-
-
-
-
- Limitations
+## Configuration
-- **Screen-capture invisibility does not work on Linux.** The overlay stays hidden from screen shares and recordings only on **macOS** and **Windows**. This relies on Electron's `setContentProtection`, which maps to `NSWindowSharingNone` on macOS and `WDA_EXCLUDEFROMCAPTURE` on Windows. Electron provides **no equivalent on Linux** (neither X11 nor Wayland), so on Linux the call is a silent no-op and the overlay **will be visible** to anyone you screen-share with. This is a platform limitation, not a bug — there is no window flag on Linux that excludes a window from framebuffer capture. If you need capture-invisibility, run OpenCluely on macOS or Windows. As a partial workaround on Linux, share a single application window instead of your entire screen, or place the overlay on a monitor you are not sharing.
+The main environment values are documented in [`env.example`](env.example):
-
+| Variable | Purpose |
+| --- | --- |
+| `GOOGLE_CLOUD_PROJECT` | Billed project used for Vertex AI |
+| `GOOGLE_CLOUD_LOCATION` | Vertex AI region; defaults to `europe-west4` |
+| `GEMINI_LIVE_MODEL` | Gemini Live model resource name |
+| `SYSTEM_AUDIO_COMMAND` | Optional explicit path to `SystemAudioDump` |
+| `FFMPEG_COMMAND` | `ffmpeg` command or absolute path |
+| `CALL_COPILOT_SILENCE_MS` | Pause duration that closes a natural utterance |
+| `CALL_COPILOT_MAX_UTTERANCE_MS` | Maximum continuous utterance duration |
+| `CALL_COPILOT_VAD_FLOOR` | Minimum voice-activity energy floor |
+## Build
+```bash
+npm run build:mac
+```
-## Privacy and ethics
+The build produces Intel and Apple Silicon DMG and ZIP artifacts under `dist/`. Local builds may use an installed Apple signing identity automatically. This repository does not contain signing or notarization credentials.
-OpenCluely collects no data and sends no telemetry. Processing happens locally, and your session stays on your device. Requests to the AI provider are encrypted in transit.
+## Privacy, permissions, and responsible use
-The app is built for learning and practice. You are responsible for following the rules of any interview you take and the policies of the companies involved.
+- OpenCluely starts Live Screen and Call Copilot only after an explicit user action.
+- Screen and audio context is sent to the configured Google Cloud Vertex AI project when those features are active.
+- The app writes local diagnostic logs but does not add a telemetry service.
+- The native Electron overlay and the browser rail have different capture behavior; the browser rail is not private from screen sharing.
+- Record or process a conversation only with appropriate consent and where recording and AI assistance are allowed.
+- Use the project for learning, accessibility, personal productivity, and disclosed assistance—not to violate interview, assessment, workplace, or platform rules.
-## License
+## Current limitations
-Released under the MIT License. See [LICENSE](LICENSE) for details.
+- Gemini Live system-audio capture is currently implemented for macOS.
+- The browser rail must be reactivated after navigation.
+- Builds are not notarized by this fork.
+- Screen-capture exclusion depends on the operating system and capture software and should not be treated as a security boundary.
+- Gemini Live model availability, quotas, and regions depend on your Google Cloud account.
-## Acknowledgments
+## Validation performed
-- Google Gemini for the AI reasoning
-- Azure Speech and OpenAI Whisper for optional voice input
-- Electron for the cross platform desktop runtime
-- [Vysper by varun-singhh](https://github.com/varun-singhh/Vysper) for UI and structure inspiration
+- JavaScript, shell, JSON, and whitespace checks
+- Browser bridge test: extension origin accepted, webpage origin rejected
+- Credential and machine-specific identifier scan
+- Packaged ASAR inspection confirming legacy Whisper runtime and local coaching artifacts are excluded
+- Successful macOS x64 and arm64 DMG/ZIP builds
-
+## Upstream and license
-Built by [TechyCSR](https://techycsr.dev). If OpenCluely helped you, consider giving it a star ⭐
+This project is a fork of [TechyCSR/OpenCluely](https://github.com/TechyCSR/OpenCluely). The upstream project, its authors, and prior contributors remain credited in Git history. New fork-specific work is proposed upstream separately where appropriate.
-
+Released under the [Apache License 2.0](LICENSE).
diff --git a/browser-extension/background.js b/browser-extension/background.js
new file mode 100644
index 0000000..990992e
--- /dev/null
+++ b/browser-extension/background.js
@@ -0,0 +1,95 @@
+const WS_URL = 'ws://127.0.0.1:17321/?token=oc_local_7f86c1d293b74b5eb8ae3fd4';
+const ALLOWED_COMMANDS = new Set([
+ 'toggle-live-screen',
+ 'toggle-call-copilot',
+ 'send-chat-message',
+ 'get-status'
+]);
+
+const railTabs = new Set();
+let socket;
+let reconnectTimer;
+let requestSequence = 0;
+
+const broadcastToRails = message => {
+ railTabs.forEach(tabId => {
+ chrome.tabs.sendMessage(tabId, message).catch(() => railTabs.delete(tabId));
+ });
+};
+
+const setConnection = connected => {
+ broadcastToRails({ type: 'bridge-connection', connected });
+};
+
+const sendCommand = (command, payload = {}) => {
+ if (!ALLOWED_COMMANDS.has(command) || socket?.readyState !== WebSocket.OPEN) return false;
+ socket.send(JSON.stringify({
+ type: 'command',
+ command,
+ payload,
+ requestId: String(++requestSequence)
+ }));
+ return true;
+};
+
+const scheduleReconnect = () => {
+ clearTimeout(reconnectTimer);
+ if (railTabs.size) reconnectTimer = setTimeout(connect, 1500);
+};
+
+const connect = () => {
+ if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return;
+ clearTimeout(reconnectTimer);
+ try {
+ socket = new WebSocket(WS_URL);
+ } catch (_) {
+ scheduleReconnect();
+ return;
+ }
+ socket.addEventListener('open', () => {
+ setConnection(true);
+ sendCommand('get-status');
+ });
+ socket.addEventListener('message', event => {
+ try {
+ broadcastToRails({ type: 'bridge-message', message: JSON.parse(event.data) });
+ } catch (_) {}
+ });
+ socket.addEventListener('close', () => {
+ setConnection(false);
+ scheduleReconnect();
+ });
+ socket.addEventListener('error', () => socket.close());
+};
+
+chrome.action.onClicked.addListener(async tab => {
+ if (!tab.id) return;
+ railTabs.add(tab.id);
+ try {
+ await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
+ connect();
+ } catch (_) {
+ railTabs.delete(tab.id);
+ await chrome.action.setBadgeText({ tabId: tab.id, text: 'ERR' });
+ await chrome.action.setBadgeBackgroundColor({ tabId: tab.id, color: '#b42318' });
+ }
+});
+
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message?.type === 'rail-ready' && sender.tab?.id) {
+ railTabs.add(sender.tab.id);
+ connect();
+ sendResponse({ connected: socket?.readyState === WebSocket.OPEN });
+ return false;
+ }
+ if (message?.type === 'bridge-command') {
+ sendResponse({ sent: sendCommand(message.command, message.payload || {}) });
+ return false;
+ }
+ return false;
+});
+
+chrome.tabs.onRemoved.addListener(tabId => railTabs.delete(tabId));
+chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status === 'loading') railTabs.delete(tabId);
+});
diff --git a/browser-extension/content.js b/browser-extension/content.js
new file mode 100644
index 0000000..8e524ff
--- /dev/null
+++ b/browser-extension/content.js
@@ -0,0 +1,286 @@
+(() => {
+ if (document.getElementById('opencluely-browser-rail-host')) return;
+
+ const host = document.createElement('div');
+ host.id = 'opencluely-browser-rail-host';
+ host.style.cssText = 'all:initial;position:fixed;inset:0 0 auto auto;z-index:2147483647;pointer-events:none;';
+ document.documentElement.appendChild(host);
+ const root = host.attachShadow({ mode: 'open' });
+
+ root.innerHTML = `
+
+ `;
+
+ const rail = root.querySelector('.rail');
+ const mark = root.querySelector('.mark');
+ const state = root.querySelector('.state');
+ const answer = root.querySelector('.answer');
+ const intent = root.querySelector('.intent-text');
+ const collapse = root.querySelector('.collapse');
+ const liveScreen = root.querySelector('.live-screen');
+ const liveAudio = root.querySelector('.live-audio');
+ const input = root.querySelector('textarea');
+ const send = root.querySelector('.send');
+
+ let currentText = '';
+
+ const setConnection = connected => {
+ mark.classList.toggle('connected', connected);
+ state.textContent = connected ? 'connected' : 'offline';
+ };
+
+ const setAnswer = text => {
+ currentText = String(text || '');
+ answer.textContent = currentText || 'Waiting for an answer…';
+ answer.classList.toggle('empty', !currentText);
+ };
+
+ const deriveIntent = text => {
+ const clean = String(text || '').replace(/\*\*/g, '');
+ const match = clean.match(/(?:^|\n)Intent\s*[—:-]\s*([^\n]+)/i);
+ if (match) intent.textContent = match[1].trim();
+ };
+
+ const command = (name, payload = {}) => {
+ chrome.runtime.sendMessage({ type: 'bridge-command', command: name, payload });
+ };
+
+ const handleEvent = ({ channel, data = {} }) => {
+ if (channel === 'transcription-llm-response-start') {
+ setAnswer('');
+ rail.classList.remove('collapsed');
+ collapse.textContent = '›';
+ } else if (channel === 'transcription-llm-response-chunk') {
+ setAnswer(currentText + String(data.delta || ''));
+ deriveIntent(currentText);
+ answer.scrollTop = answer.scrollHeight;
+ } else if (channel === 'transcription-llm-response') {
+ setAnswer(data.response || data.content || currentText);
+ deriveIntent(currentText);
+ } else if (channel === 'llm-response') {
+ setAnswer(data.response || data.content || '');
+ deriveIntent(currentText);
+ } else if (channel === 'live-screen-status') {
+ liveScreen.classList.toggle('active', !!data.active);
+ liveScreen.textContent = data.active ? 'Screen active' : 'Live Screen';
+ } else if (channel === 'call-copilot-status') {
+ const active = !!(data.isCapturing || data.liveConnected);
+ liveAudio.classList.toggle('active', active);
+ liveAudio.textContent = active ? 'Audio active' : 'Live Audio';
+ } else if (channel === 'llm-error' || channel === 'ocr-error') {
+ setAnswer(`OpenCluely error: ${data.error || 'Unknown error'}`);
+ } else if (channel === 'session-cleared') {
+ intent.textContent = 'Waiting for screen context…';
+ setAnswer('');
+ }
+ };
+
+ const handleBridgeMessage = message => {
+ if (message.type === 'event') handleEvent(message);
+ if (message.type === 'command-result' && message.result?.status) {
+ const statusResult = message.result.status;
+ handleEvent({ channel: 'live-screen-status', data: statusResult.liveScreen || {} });
+ handleEvent({ channel: 'call-copilot-status', data: statusResult.callCopilot || {} });
+ }
+ };
+
+ const setCollapsed = collapsed => {
+ rail.classList.toggle('collapsed', collapsed);
+ collapse.textContent = collapsed ? '‹' : '›';
+ collapse.setAttribute('aria-expanded', String(!collapsed));
+ collapse.setAttribute('aria-label', collapsed ? 'Expand OpenCluely' : 'Collapse OpenCluely');
+ collapse.title = collapsed ? 'Expand' : 'Collapse';
+ };
+ collapse.addEventListener('click', () => setCollapsed(!rail.classList.contains('collapsed')));
+ liveScreen.addEventListener('click', () => command('toggle-live-screen'));
+ liveAudio.addEventListener('click', () => command('toggle-call-copilot'));
+ const sendMessage = () => {
+ const text = input.value.trim();
+ if (!text) return;
+ command('send-chat-message', { text });
+ input.value = '';
+ };
+ send.addEventListener('click', sendMessage);
+ input.addEventListener('keydown', event => {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ sendMessage();
+ }
+ });
+ root.addEventListener('click', event => event.stopPropagation());
+ root.addEventListener('keydown', event => event.stopPropagation());
+ chrome.runtime.onMessage.addListener(message => {
+ if (message?.type === 'bridge-connection') setConnection(!!message.connected);
+ if (message?.type === 'bridge-message' && message.message) handleBridgeMessage(message.message);
+ });
+ chrome.runtime.sendMessage({ type: 'rail-ready' }, response => {
+ setConnection(!!response?.connected);
+ });
+})();
diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json
new file mode 100644
index 0000000..0f25e5f
--- /dev/null
+++ b/browser-extension/manifest.json
@@ -0,0 +1,19 @@
+{
+ "manifest_version": 3,
+ "name": "OpenCluely Browser Rail",
+ "version": "1.1.0",
+ "description": "Shows local OpenCluely responses in the browser tab you explicitly activate.",
+ "permissions": [
+ "activeTab",
+ "scripting"
+ ],
+ "host_permissions": [
+ "http://127.0.0.1/*"
+ ],
+ "background": {
+ "service_worker": "background.js"
+ },
+ "action": {
+ "default_title": "Show OpenCluely rail in this tab"
+ }
+}
diff --git a/chat.html b/chat.html
index b0f68c4..4e52ad2 100644
--- a/chat.html
+++ b/chat.html
@@ -14,8 +14,9 @@
margin: 0;
padding: 0;
overflow: hidden;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- sans-serif;
+ font-family: "Avenir Next", "Helvetica Neue", sans-serif;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
}
.chat-container {
@@ -26,7 +27,7 @@
rgba(0, 0, 0, 0.3) 0%,
rgba(20, 20, 20, 0.4) 100%
);
- backdrop-filter: blur(25px);
+ backdrop-filter: none;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.15);
@@ -42,7 +43,7 @@
justify-content: space-between;
-webkit-app-region: drag;
background: rgba(0, 0, 0, 0.2);
- backdrop-filter: blur(10px);
+ backdrop-filter: none;
cursor: move;
flex-shrink: 0;
}
@@ -80,7 +81,7 @@
height: 8px;
border-radius: 50%;
background: #ff4757;
- animation: pulse 2s infinite;
+ animation: none;
display: none;
box-shadow: 0 0 10px rgba(255, 71, 87, 0.5);
}
@@ -161,8 +162,15 @@
background: rgba(255, 152, 0, 0.1);
}
.message.assistant {
- background: rgba(156, 39, 176, 0.1);
- border-left: 3px solid #9c27b0;
+ background: linear-gradient(145deg, rgba(31, 44, 38, 0.92), rgba(18, 23, 21, 0.92));
+ border: 1px solid rgba(142, 227, 179, 0.16);
+ border-left: 4px solid #8ee3b3;
+ border-radius: 10px;
+ padding: 16px 18px;
+ color: #f6f7f5;
+ font-size: 17px;
+ line-height: 1.55;
+ letter-spacing: -0.01em;
/* Removed all height restrictions and overflow hidden */
display: block;
}
@@ -173,6 +181,8 @@
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
+ font-size: 17px;
+ line-height: 1.55;
}
/* Ensure all content in assistant messages is fully visible */
@@ -323,12 +333,12 @@
color: rgba(255, 255, 255, 1);
}
- .message.assistant h1 { font-size: 16px; }
- .message.assistant h2 { font-size: 15px; }
- .message.assistant h3 { font-size: 14px; }
+ .message.assistant h1 { font-size: 24px; }
+ .message.assistant h2 { font-size: 21px; }
+ .message.assistant h3 { font-size: 19px; }
.message.assistant h4,
.message.assistant h5,
- .message.assistant h6 { font-size: 13px; }
+ .message.assistant h6 { font-size: 17px; }
/* Ensure paragraphs are fully visible */
.message.assistant p {
diff --git a/env.example b/env.example
index d770c1e..590865c 100644
--- a/env.example
+++ b/env.example
@@ -1,31 +1,18 @@
-# Google Gemini API Configuration
-# Get your API key from: https://makersuite.google.com/app/apikey
-GEMINI_API_KEY=your_gemini_api_key_here
+# Text LLM provider: antigravity (Google OAuth via agy CLI) or gemini (Vertex AI)
+LLM_PROVIDER=gemini
+ANTIGRAVITY_COMMAND=
+ANTIGRAVITY_MODEL=Gemini 3.1 Pro (Low)
+ANTIGRAVITY_TIMEOUT_MS=120000
-# Speech Recognition Configuration
-# Choose one provider: azure or whisper
-SPEECH_PROVIDER=whisper
+# Vertex AI and Gemini Live. Authenticate once with:
+# gcloud auth application-default login
+GOOGLE_CLOUD_PROJECT=
+GOOGLE_CLOUD_LOCATION=europe-west4
+GEMINI_LIVE_MODEL=gemini-live-2.5-flash-native-audio
-# Optional: Azure Speech Services Configuration
-AZURE_SPEECH_KEY=your_azure_speech_key_here
-AZURE_SPEECH_REGION=your_azure_region_here
-
-# Optional: Local OpenAI Whisper Configuration
-# Requires a local Whisper CLI installation, for example:
-# pip install openai-whisper
-# brew install ffmpeg sox
-# Use `whisper`, `python3 -m whisper`, or on Windows `.venv-whisper/Scripts/whisper.exe`
-WHISPER_COMMAND=whisper
-# Optional: where Whisper model weights are stored. Leave unset to use a stable
-# app-data folder (recommended). Set an ABSOLUTE path to override; a relative
-# path is ignored because it cannot be resolved reliably in packaged builds.
-# WHISPER_MODEL_DIR=
-WHISPER_MODEL=small
-WHISPER_LANGUAGE=auto
-WHISPER_SEGMENT_MS=4000
-WHISPER_DEVICE=auto
-WHISPER_PYTHON=
-WHISPER_CAPTURE_MODE=vad
-WHISPER_RESPONSE_TARGET=both
-WHISPER_MANUAL_MAX_MS=90000
-WHISPER_GPU_IDLE_MS=60000
+# macOS Call Copilot (system audio -> Gemini Live)
+SYSTEM_AUDIO_COMMAND=
+FFMPEG_COMMAND=ffmpeg
+CALL_COPILOT_SILENCE_MS=800
+CALL_COPILOT_MAX_UTTERANCE_MS=20000
+CALL_COPILOT_VAD_FLOOR=0.006
diff --git a/index.html b/index.html
index 31e37f5..a8d2844 100644
--- a/index.html
+++ b/index.html
@@ -22,7 +22,7 @@
rgba(0, 0, 0, 0.4) 0%,
rgba(20, 20, 20, 0.5) 100%
);
- backdrop-filter: blur(20px);
+ backdrop-filter: none;
border-radius: 8px;
display: flex;
align-items: center;
@@ -73,7 +73,7 @@
.command-item.recording i {
color: #ff4757;
text-shadow: 0 0 10px rgba(255, 71, 87, 0.5);
- animation: pulse 2s infinite;
+ animation: none;
}
.command-item.active {
@@ -103,6 +103,32 @@
box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.25) inset;
}
+ #exitButton {
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ color: rgba(248, 113, 113, 0.82);
+ }
+
+ #exitButton i {
+ font-size: 13px;
+ }
+
+ #exitButton:hover {
+ background: rgba(239, 68, 68, 0.18);
+ color: #fca5a5;
+ box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.28) inset;
+ }
+
+ #exitButton:focus-visible {
+ outline: 2px solid rgba(248, 113, 113, 0.72);
+ outline-offset: 2px;
+ }
+
.command-separator {
width: 1px;
height: 16px;
@@ -127,7 +153,7 @@
.status-dot.interactive {
background-color: #10b981; /* Green for active/interactive */
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
- animation: pulse-green 2s infinite;
+ animation: none;
}
.status-dot.non-interactive {
@@ -263,7 +289,7 @@
rgba(20, 20, 20, 0.7) 0%,
rgba(10, 10, 10, 0.6) 100%
);
- backdrop-filter: blur(18px);
+ backdrop-filter: none;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
color: rgba(255, 255, 255, 0.95);
@@ -358,30 +384,34 @@
⌘⇧S
-