Claude-skill-registry chrome-extension-brief-hq-brief-wiggum
Chrome extension development patterns and conventions
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/chrome-extension-brief-hq-brief-wiggum" ~/.claude/skills/majiayu000-claude-skill-registry-chrome-extension-brief-hq-brief-wiggum && rm -rf "$T"
manifest:
skills/data/chrome-extension-brief-hq-brief-wiggum/SKILL.mdsource content
Chrome Extension Development
Architecture Overview
Brief's Chrome extension provides AI chat directly in the browser using Side Panel architecture.
Key Components:
- Manifest V3: Modern Chrome extension format
- Side Panel: Chrome's native side panel API (not content scripts)
- OAuth Authentication: chrome.identity.launchWebAuthFlow with PKCE
- Shared Business Logic: Imports from
package@briefhq/chat-ui - Context Awareness: Tracks active tab URL for contextual assistance
Side Panel Architecture
Brief uses Chrome's Side Panel API, NOT content script injection:
// sidepanel.tsx - Main entry point export default function SidePanel() { const [accessToken, setAccessToken] = useState<string | null>(null); const [contextUrl, setContextUrl] = useState<string | undefined>(); // Check auth on mount useEffect(() => { const token = await getValidAccessToken(); setAccessToken(token); }, []); // Track active tab URL useEffect(() => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { setContextUrl(tabs[0]?.url); }); chrome.tabs.onActivated.addListener(handleTabChange); chrome.tabs.onUpdated.addListener(handleUrlChange); }, []); return <ChatInterface accessToken={accessToken} contextUrl={contextUrl} />; }
Manifest Configuration:
{ "manifest_version": 3, "permissions": ["sidePanel", "activeTab", "storage", "tabs", "identity"], "side_panel": { "default_path": "sidepanel.html" }, "commands": { "toggle-side-panel": { "suggested_key": { "default": "Ctrl+Shift+B", "mac": "Command+Shift+B" } } } }
Shared Code from @briefhq/chat-ui
The extension reuses business logic from the
@briefhq/chat-ui package (monorepo sibling):
Shared Hooks
| Hook | Purpose |
|---|---|
| Manages streaming, messages, conversation state |
| Load/save/delete conversations |
| Fetch and select chat presets |
| Track token usage in context window |
| Submit thumbs up/down to Helicone |
| File selection, validation, removal |
| Document search and @-mention selection |
Shared UI Components
| Component | Purpose |
|---|---|
, | Scroll container |
, | Message bubbles |
| Markdown rendering |
, , , , | Tool call display |
, , | Extended thinking UI |
| Loading indicator |
Import Pattern
// ✅ GOOD - Import shared hooks and components from @briefhq/chat-ui import { useChatTransport, useConversationHistory, usePresets, Conversation, Message, Response, Tool, Loader, } from "@briefhq/chat-ui"; // Extension-specific orchestration export function ChatInterface({ accessToken, contextUrl }: Props) { const transport = useChatTransport({ defaultModel: "claude-sonnet-4-5" }); const history = useConversationHistory({ api, onConversationLoaded }); const presets = usePresets({ api }); // Extension-specific logic here return ( <div> <ChatHeader presets={presets} onSignOut={onSignOut} /> <Conversation> {messages.map(msg => <Message key={msg.id} message={msg} />)} </Conversation> <ChatInputArea onSubmit={handleSubmit} /> </div> ); }
// ❌ BAD - Don't recreate what exists in @briefhq/chat-ui export function ChatInterface() { // Don't reimplement shared business logic const [messages, setMessages] = useState([]); const [isStreaming, setIsStreaming] = useState(false); // This should use useChatTransport instead const handleSubmit = async (input: string) => { // Streaming logic... }; }
When to Create Extension-Specific Components
Create components in
packages/chrome-extension/components/ ONLY when:
- Chrome API Integration: Component uses chrome.tabs, chrome.storage, chrome.identity
- Extension-Specific UI: Component is unique to side panel context (e.g., sign-in flow, header with sign-out)
- Extension-Specific Configuration: Wrapper needed for extension constraints
Example - Extension-specific header:
// components/chat/views/ChatHeader.tsx // Extension-specific because it has sign-out, model selector, history toggle export function ChatHeader({ presets, onSignOut }: Props) { return ( <header className="flex items-center justify-between px-4 py-3 border-b"> <PresetSelector presets={presets} /> <ModelSelector models={AVAILABLE_MODELS} /> <button onClick={onSignOut}>Sign out</button> </header> ); }
OAuth Authentication
Brief uses chrome.identity API with PKCE for OAuth 2.0:
// lib/oauth.ts // Start OAuth flow export async function startOAuthFlow() { // Generate PKCE challenge const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); // Launch web auth flow const redirectUrl = chrome.identity.getRedirectURL(); const authUrl = `${BRIEF_URL}/oauth/authorize?` + `client_id=${CLIENT_ID}&` + `redirect_uri=${redirectUrl}&` + `response_type=code&` + `code_challenge=${codeChallenge}&` + `code_challenge_method=S256`; const responseUrl = await chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true, }); // Exchange code for tokens const code = extractCodeFromUrl(responseUrl); const tokens = await exchangeCodeForTokens(code, codeVerifier); // Store in chrome.storage.local await chrome.storage.local.set({ access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: Date.now() + tokens.expires_in * 1000, }); return tokens; } // Get valid token (auto-refresh if needed) export async function getValidAccessToken(): Promise<string | null> { const { access_token, expires_at, refresh_token } = await chrome.storage.local.get(["access_token", "expires_at", "refresh_token"]); if (!access_token) return null; // Check if token needs refresh if (Date.now() >= expires_at - 60000) { return await refreshAccessToken(refresh_token); } return access_token; }
API Integration
Extension calls Brief API with OAuth Bearer tokens:
// hooks/use-extension-api.ts export function useExtensionApi(accessToken: string) { return { async chat(messages, options) { const response = await fetch(`${BRIEF_URL}/api/v1/chat`, { method: "POST", headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ messages, model: options.model, contextUrl: options.contextUrl, // Current tab URL }), }); return response.body; // Streaming response }, async fetchConversations() { const response = await fetch(`${BRIEF_URL}/api/v1/conversations`, { headers: { "Authorization": `Bearer ${accessToken}` }, }); return response.json(); }, async searchDocuments(query: string) { const response = await fetch( `${BRIEF_URL}/api/v1/documents/search?q=${query}`, { headers: { "Authorization": `Bearer ${accessToken}` }, } ); return response.json(); }, }; }
Component Organization
packages/chrome-extension/ ├── sidepanel.tsx # Main entry (auth + ChatInterface) ├── background.ts # Service worker (keyboard shortcuts) ├── lib/ │ ├── oauth.ts # OAuth 2.0 with PKCE │ └── utils.ts # Utility functions ├── components/chat/ │ ├── ChatInterface.tsx # Main orchestrator (~400 LOC) │ ├── hooks/ │ │ └── use-extension-api.ts # Extension-specific API wrapper │ ├── views/ # Extension-specific UI │ │ ├── ChatHeader.tsx # Header with sign-out │ │ ├── ChatHistoryView.tsx # History sidebar │ │ ├── ChatEmptyState.tsx # Empty state │ │ └── ChatInputArea.tsx # Input with @-mentions │ ├── MentionExtension.tsx # TipTap mention config │ ├── MentionList.tsx # Mention dropdown │ ├── ContextRing.tsx # Context indicator │ └── EnhancedFileContent.tsx # File preview └── __tests__/ # Vitest unit tests
Rule: Extension components ONLY in
components/chat/. Shared components come from @briefhq/chat-ui.
Development Workflow
Local Setup
cd packages/chrome-extension pnpm install # Create environment file cp .env.dev.example .env.dev # Start dev server (hot reload) pnpm run dev
Load Extension in Chrome
- Open
chrome://extensions/ - Enable "Developer mode"
- Click "Load unpacked"
- Select
packages/chrome-extension/build/chrome-mv3-dev
Build for Different Environments
| Environment | Command | Target |
|---|---|---|
| Dev | | localhost:3000 |
| Staging | | staging.briefhq.ai |
| QA | | app.briefhq.ai (internal) |
| Production | | app.briefhq.ai (public) |
Package for Distribution
# QA build (.crx for internal testing) pnpm run package:qa # Production build (for Chrome Web Store) pnpm run package:production
Testing
Brief extension uses Vitest for unit tests:
import { describe, it, expect, vi } from "vitest"; import { render, waitFor } from "@testing-library/react"; import { ChatInterface } from "./ChatInterface"; // Mock Chrome APIs vi.mock("chrome", () => ({ tabs: { query: vi.fn(), onActivated: { addListener: vi.fn(), removeListener: vi.fn() }, onUpdated: { addListener: vi.fn(), removeListener: vi.fn() }, }, storage: { local: { get: vi.fn(), set: vi.fn(), }, }, })); describe("ChatInterface", () => { it("renders chat UI when authenticated", () => { const { getByRole } = render( <ChatInterface accessToken="test-token" contextUrl="https://example.com" /> ); expect(getByRole("textbox")).toBeInTheDocument(); }); it("passes contextUrl to chat API", async () => { const { getByRole, getByText } = render( <ChatInterface accessToken="test-token" contextUrl="https://example.com/page" /> ); const input = getByRole("textbox"); await userEvent.type(input, "Summarize this page"); await userEvent.click(getByText("Send")); // Verify contextUrl passed to API expect(fetchMock).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.stringContaining("https://example.com/page"), }) ); }); });
Run Tests
pnpm test # Run all tests pnpm run test:watch # Watch mode pnpm run test:coverage # Coverage report
Key Differences from Web App
| Feature | Web App | Extension |
|---|---|---|
| Architecture | Next.js pages | Side Panel |
| Authentication | Clerk session cookies | OAuth Bearer tokens |
| API URL | Relative () | Absolute () |
| Context | Current page (server-side) | Active tab URL (chrome.tabs API) |
| Presets | All presets | Excludes "onboarding" preset |
| Storage | Supabase + Clerk | chrome.storage.local for tokens |
Common Patterns
Passing Context URL
// ✅ GOOD - Use current tab URL for context export function ChatInterface({ contextUrl }: Props) { const handleSubmit = async (input: string) => { await api.chat(messages, { contextUrl, // Current tab URL model: selectedModel, }); }; }
Model Selection
// ✅ GOOD - Store model selection in state const [selectedModel, setSelectedModel] = useState<ModelId>("claude-sonnet-4-5"); // Pass to useChatTransport const transport = useChatTransport({ defaultModel: selectedModel, });
File Attachments
// ✅ GOOD - Use useFileAttachments hook from @briefhq/chat-ui const fileAttachments = useFileAttachments({ maxFiles: 10, maxSize: 32 * 1024 * 1024, // 32MB allowedTypes: ["image/*", "application/pdf", "text/*"], }); // In UI <ChatInputArea files={fileAttachments.files} onFilesSelected={fileAttachments.addFiles} onFileRemove={fileAttachments.removeFile} />
Document @-Mentions
// ✅ GOOD - Use useMentions hook from @briefhq/chat-ui const mentions = useMentions({ api, onMentionSelected: (doc: MentionDocument) => { // Append to input }, }); // In TipTap editor <Editor extensions={[ StarterKit, Mention.configure({ suggestion: mentions.suggestionOptions, }), ]} />
Troubleshooting
OAuth Issues
Problem: "Authentication failed" error
- Check: Extension ID in OAuth client redirect URIs
- Fix: Add
to allowed redirect URIshttps://{extension-id}.chromiumapp.org/
Problem: Token expired
- Check: Token refresh logic in
lib/oauth.ts - Fix: Implement automatic refresh 60 seconds before expiry
API Issues
Problem: CORS errors
- Check:
in manifest.jsonhost_permissions - Fix: Add API domain to
host_permissions
Build Issues
Problem: Hot reload not working
- Check: Plasmo dev server running
- Fix: Run
and reload extensionpnpm run dev
Documentation References
- Chrome Extension Architecture:
/docs/CHROME_EXTENSION_ARCHITECTURE.md - Extension README:
/packages/chrome-extension/README.md - Shared Chat UI:
/packages/chat-ui/
Related Code
- OAuth flow:
packages/chrome-extension/lib/oauth.ts - Chat interface:
packages/chrome-extension/components/chat/ChatInterface.tsx - Side panel entry:
packages/chrome-extension/sidepanel.tsx - Extension API:
packages/chrome-extension/components/chat/hooks/use-extension-api.ts