This guide covers everything you need to develop, test, debug, and extend Xcho.
- Node.js >= 18.0.0
- Chrome browser (for loading the extension)
- Gemini API key from Google AI Studio
# Clone the repository
git clone <repository-url>
cd xcho
# Install dependencies
npm install
# Start development server
npm run devAfter the dev server starts, load the extension in Chrome:
- Navigate to
chrome://extensions/ - Enable Developer mode (toggle in the top right)
- Click Load unpacked
- Select the
distfolder in the project root
| Script | Command | Description |
|---|---|---|
| Dev | npm run dev |
Start Vite dev server with HMR |
| Build | npm run build |
TypeScript check + production build to dist/ |
| Test | npm test |
Run test suite once |
| Test (watch) | npm run test:watch |
Run tests in watch mode |
Xcho uses @crxjs/vite-plugin for Chrome extension development with Vite. Reload behavior differs by component:
| Component | Hot reload? | When to manually reload |
|---|---|---|
| Side Panel | Yes (Vite HMR) | Rarely -- HMR handles most changes |
| Content Script | No | After any change to src/content/ |
| Background Worker | No | After any change to src/background/ |
| Manifest | No | After any change to manifest.json |
To reload the extension manually:
- Go to
chrome://extensions/ - Click the refresh button on the Xcho card
- Refresh the X.com tab (Cmd+Shift+R / Ctrl+Shift+R)
npm run buildThis runs TypeScript type checking (tsc) followed by Vite's production build. The output goes to dist/. Load this folder as an unpacked extension for production-grade testing.
Xcho uses Vitest with a jsdom environment for unit testing.
Configuration lives in vite.config.ts:
test: {
globals: true, // vi, describe, it, expect available globally
environment: 'jsdom', // DOM APIs available in tests
setupFiles: ['./tests/setup.ts'],
include: ['tests/**/*.test.ts'],
}tests/
setup.ts # Global setup: Chrome API mocks, beforeEach cleanup
mocks/
chrome.ts # In-memory chrome.storage mock factory
gemini.ts # @google/generative-ai mock factory
The setup file registers a globalThis.chrome mock before tests run, providing:
chrome.storage.local-- In-memory key-value store (viacreateChromeStorageMock)chrome.storage.session-- Separate in-memory storechrome.runtime.sendMessage-- Vitest mock functionchrome.runtime.onMessage.addListener/removeListener-- Vitest mock functions
All mocks are reset between tests via beforeEach to prevent state leakage.
Chrome Storage Mock (tests/mocks/chrome.ts):
- Creates an in-memory
Map-backed implementation ofchrome.storageareas - Supports
get(single key, array, null for all),set,remove,clear - Exposes
_reset()for test cleanup and_storefor inspection
Gemini Mock (tests/mocks/gemini.ts):
createMockGenerativeModel()-- Returns a model with mockgenerateContentandgenerateContentStreamcreateMockGoogleGenerativeAI()-- Returns a mock SDK instance withgetGenerativeModelsetupGeminiMock()-- Callsvi.mock('@google/generative-ai')for module-level mocking
- Create a test file in
tests/matching the patterntests/**/*.test.ts - Chrome APIs are automatically available via the global setup
- For tests that call Gemini, use
setupGeminiMock()at module scope:
import { setupGeminiMock } from './mocks/gemini';
const mockAI = setupGeminiMock();
describe('generateComment', () => {
it('should return generated text', async () => {
// mockAI._mockModel.generateContent is pre-configured
// to return 'Mock generated response'
const result = await generateComment(tweetData, 'friendly', 'fake-key');
expect(result.comment).toBe('Mock generated response');
});
});- For storage tests, the global mock handles everything:
import { storage } from '../src/utils/storage';
describe('storage', () => {
it('should persist and retrieve values', async () => {
await storage.set('apiKey', 'test-key');
const value = await storage.get('apiKey');
expect(value).toBe('test-key');
});
});For a comprehensive step-by-step debugging guide, see DEBUGGING.md.
Each extension component has its own console. You need to open the right DevTools to see logs:
| Component | How to access console | Log prefix |
|---|---|---|
| Content Script | F12 on the X.com page | Emoji-prefixed (e.g., [rocket] X Comment Helper content script loaded) |
| Background Worker | chrome://extensions/ -> click "service worker" link | Plain text |
| Side Panel | Right-click inside the side panel -> Inspect | Emoji-prefixed |
"Failed to send message" in content script console: The background service worker has gone inactive. Reload the extension at chrome://extensions/ and refresh the X.com page.
Tweet text not extracted: X.com may have changed its DOM structure. Test the selectors manually in the X.com console:
document.querySelector('article[data-testid="tweet"]')
document.querySelector('[data-testid="tweetText"]')?.textContentSide panel shows "Select a Tweet" after clicking: The message didn't reach the side panel. Check that:
- The background worker received the
TWEET_CLICKEDmessage - The side panel's
GET_CURRENT_TWEETrequest returned data
-
Add the value to
CommentToneinsrc/types/index.ts:export type CommentTone = 'friendly' | 'professional' | 'empathetic' | 'humorous' | 'sarcastic';
-
Add tone instruction in
TONE_INSTRUCTIONSinsrc/utils/gemini.ts:sarcastic: 'Dry and cutting. Says the opposite of what it means with a straight face.' -
Add display label in
TONE_LABELSinsrc/sidepanel/App.tsx:sarcastic: 'Sarcastic'
-
Add the model ID to
GeminiModelinsrc/types/index.ts:export type GeminiModel = 'gemini-3-flash-preview' | 'gemini-3-pro-preview' | 'gemini-3-ultra-preview';
-
Add pricing in
MODEL_PRICINGinsrc/utils/gemini.ts:'gemini-3-ultra-preview': { input: 5.00, output: 30.00 },
-
Add to
AVAILABLE_MODELSarray insrc/utils/gemini.ts:{ id: 'gemini-3-ultra-preview', name: 'Gemini 3 Ultra', description: 'Maximum quality' },
Follow the same pattern as tones: update CommentStance type, add to STANCE_INSTRUCTIONS in gemini.ts, add to STANCE_LABELS in App.tsx.
The comment generation prompt lives in both generateComment and generateCommentStream in src/utils/gemini.ts. These two functions contain duplicate prompt templates -- if you modify one, update the other to match.
Key areas to modify:
<voice>section for writing rulesSTANCE_INSTRUCTIONSobject for stance behaviorTONE_INSTRUCTIONSobject for tone descriptions<constraints>section for numbered rules<self-check>section for verification items
See docs/prompt-engineering.md for detailed design rationale.
- Strict mode enabled (
strict: truein tsconfig.json) - No unused locals or parameters enforced
- Target: ES2020
- Module resolution: Bundler mode (Vite)
- JSX: react-jsx (automatic React import)
All shared types live in src/types/index.ts:
TweetData,CommentSuggestion-- Data modelsCommentTone,CommentLength,CommentStance,GeminiModel-- Union type enumsStorageData-- Keys and types forchrome.storage.localSessionStorageData-- Keys and types forchrome.storage.sessionMessagePayload-- Chrome messaging format with discriminatedtypefieldTokenUsage,TokenCost,GenerationResult-- API result typesPersonaData-- Writing style analysis resultTweetResults,SidePanelState-- Cache and session restore types
All storage access goes through typed wrappers in src/utils/storage.ts:
storage.get(key)/storage.set(key, value)--chrome.storage.localwithStorageDatatypessessionStorage.get(key)/sessionStorage.set(key, value)--chrome.storage.sessionwithSessionStorageDatatypes
Never call chrome.storage.local or chrome.storage.session directly in the side panel code. The background service worker accesses session storage directly for the tweet cache because it needs lower-level control.
All message types are defined in the MessagePayload interface:
type: 'TWEET_CLICKED' | 'GENERATE_COMMENT' | 'OPEN_SIDEPANEL' | 'GET_CURRENT_TWEET'When adding a new message type, update this union and handle it in the appropriate listener (background's onMessage or side panel's onMessage).
xcho/
manifest.json # Chrome extension manifest (V3)
sidepanel.html # Side panel entry point
vite.config.ts # Vite + CRXJS + Vitest config
tsconfig.json # TypeScript config (strict mode)
package.json # Dependencies and scripts
src/
content/
index.ts # Content script entry: click handling, SPA nav
extractors.ts # DOM extraction: timeline view + detail page
background/
index.ts # Service worker: caching, side panel lifecycle
sidepanel/
App.tsx # React UI: generation, translation, settings
App.css # Styles
main.tsx # React entry point
types/
index.ts # All shared TypeScript interfaces and types
utils/
gemini.ts # Gemini API: prompts, streaming, cost calc
persona.ts # Persona analysis: upload, analyze, persist
storage.ts # Typed wrappers for chrome.storage
tests/
setup.ts # Global test setup with Chrome API mocks
mocks/
chrome.ts # In-memory chrome.storage mock
gemini.ts # Gemini SDK mock
docs/
architecture.md # System architecture documentation
prompt-engineering.md # Prompt design documentation
development.md # This file