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. - -

- Latest release - Downloads - MIT License - Platforms -

- -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
-
- +
+ + ⌘⇧G
-
- - DSA +
+
-
- - +
+
+
+ +
+
@@ -433,6 +463,14 @@ Open chat + + + Ctrl/Cmd + + Shift + + L + + Toggle Call Copilot (system audio) + Alt diff --git a/llm-response.html b/llm-response.html index 3a4276f..32d3e42 100644 --- a/llm-response.html +++ b/llm-response.html @@ -7,6 +7,15 @@