Penpal šļø
Penpal is a lightweight Chrome Extension that serves as an AI-powered writing companion for Gmail. It listens to user typing in Gmail, triggers autocomplete suggestions using Google's Gemini API, and displays them as inline "ghost text" that can be accepted or dismissed with simple keystrokes.
Features
- šµļø Smart Compose Detection: Uses a
MutationObserverto automatically detect when a compose box is opened or active in Gmail. - š» Inline Autocomplete (Ghost Text): Renders suggestions directly inside the active compose element as grayed-out placeholder text.
- ā” Seamless Interactions:
- Press
Tabto accept the suggestion. - Press
Escapeto dismiss the suggestion. - Keep typing to automatically discard it.
- Press
- š§ Context-Aware Recommendations: Passes the subject line (if available) along with the last 300 characters of the email body to provide relevant autocomplete suggestions.
- š¦ Intelligent Rate-Limiting & Debouncing:
- Debounces key inputs by 800ms to wait until the user pauses.
- Requires the current sentence to have at least 4 words before querying suggestions.
- Imposes a minimum 3-second delay between suggestions to conserve API rate limits.
- Aborts outstanding suggestions if the user starts typing again before they resolve.
- āļø Gemini 2.5 Flash Powered: Utilizes the highly optimized
gemini-2.5-flashmodel via Google's Gemini API for low-latency autocomplete suggestions.
File Structure & Architecture
The project consists of a content script running on the Gmail webpage and a service worker running in the background.
penpal/
āāā manifest.json # Extension manifest (v3), defining permissions and content scripts
āāā package.json # Node dependencies and build scripts
āāā vite.config.ts # Vite configuration to build ESM content and background scripts
āāā tsconfig.json # TypeScript configuration
āāā .env # Local environment file containing the Gemini API Key
āāā src/ # Source files (TypeScript)
āāā background.ts # Service worker listening for GET_SUGGESTION messages
āāā content.ts # Content script injected into Gmail; orchestrates compose-detection and triggers
āāā compose-detector.ts # Watches the DOM for Gmail compose boxes
āāā trigger.ts # Implements debouncing, rate-limiting, and context formatting
āāā ghost.ts # Creates and manages the inline ghost text suggestions in the DOM
āāā gemma.ts # Integrates with the Gemini API (system prompt, retry logic, and output sanitizing)
Setup & Installation
Prerequisites
- Node.js (v18+) and npm installed.
- A Google Gemini API Key. You can get one from Google AI Studio.
1. Clone & Configure Environment
- Clone or copy the project files to your local environment.
- In the root directory, create a
.envfile (if not already present) and insert your Gemini API Key:VITE_GEMMA_API_KEY=your_actual_gemini_api_key
2. Install Dependencies
Run the following command in the project root to install the TypeScript and Vite dev dependencies:
npm install
3. Build the Project
Compile the TypeScript code using Vite:
npm run build
This command compiles the files in src/ and outputs the bundled scripts into the dist/ directory as content.js and background.js.
4. Load the Extension in Chrome
- Open Google Chrome and navigate to
chrome://extensions/. - Enable Developer mode using the toggle switch in the top-right corner.
- Click Load unpacked in the top-left corner.
- Select the root directory of the
penpalproject (the folder containingmanifest.json).
How It Works Under the Hood
- Detection: compose-detector.ts runs a
MutationObserveron the body of the document. Whenever a new Gmail compose window matchesdiv[aria-label="Message Body"][contenteditable="true"], it invokes a callback. - Interaction Hook: content.ts attaches listeners to the compose area. As the user inputs text, the trigger debouncer in trigger.ts monitors the input.
- Checking Constraints: Once the input crosses 4 words in the current sentence and the user pauses typing for 800ms, the trigger extracts the email context (including subject and recent body text).
- API Call: The content script sends a message to the background script (background.ts), which forwards it to the Gemini API (gemma.ts) using the
gemini-2.5-flashmodel. - Suggestion Rendering: When the API returns a prediction, ghost.ts appends a non-editable
spaninside the compose editor. The suggestion is styled in light gray (#9aa0a6) and has mouse events disabled. - Accept/Dismiss: A
keydownlistener in content.ts interceptsTabto accept the suggestion (removing the ghost and appending it as a standard text node) orEscapeto discard it. Any other character keystroke removes the suggestion so the user can continue typing uninterrupted.