Gemma Gem Browser AI
Build and extend Gemma Gem, an on-device AI browser assistant Chrome extension running Google's Gemma 4 model via WebGPU with no cloud dependencies.
- Skill ID
- reason-machines/trending-skills/gemma-gem-browser-ai
- Publisher
- reason-machines
- Repository
- trending-skills
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
reason-machines/trending-skills/gemma-gem-browser-aiInstalls these files- SKILL.md
What this skill tells the agent
Gemma Gem Browser AI
Skill by ara.so — Daily 2026 Skills collection.
Gemma Gem is a Chrome extension that runs Google's Gemma 4 model entirely on-device via WebGPU. It injects a chat overlay into every page and exposes a tool-calling agent loop that can read pages, click elements, fill forms, execute JavaScript, and take screenshots — all without sending data to any server.
Architecture Overview
Offscreen Document Service Worker Content Script
(Gemma 4 + Agent Loop) <-> (Message Router) <-> (Chat UI + DOM Tools)
| |
WebGPU inference Screenshot capture
Token streaming JS execution- Offscreen document (
offscreen/): Loads the ONNX model via@huggingface/transformers, runs the agent loop, streams tokens. - Service worker (
background/): Routes messages, handlestake_screenshotandrun_javascript. - Content script (
content/): Injects shadow DOM chat UI, executes DOM tools. - `agent/`: Zero-dependency module defining
ModelBackendandToolExecutorinterfaces — extractable as a standalone library.
Install & Build
# Prerequisites: Node.js 18+, pnpm
pnpm install
# Development build (logging active, source maps)
pnpm build
# Production build (errors only, minified)
pnpm build:prodLoad the extension:
- Open
chrome://extensions - Enable Developer mode
- Click Load unpacked → select
.output/chrome-mv3-dev/
Model download happens automatically on first chat open:
onnx-community/gemma-4-E2B-it-ONNX— ~500 MB (default)onnx-community/gemma-4-E4B-it-ONNX— ~1.5 GB
Models are cached in the browser's cache storage after the first run.
Key Interfaces (agent/)
ModelBackend
// agent/types.ts
export interface ModelBackend {
generate(
messages: ChatMessage[],
tools: ToolDefinition[],
options: GenerateOptions
): AsyncGenerator<StreamChunk>;
}
export interface ToolDefinition {
name: string;
description: string;
parameters: JSONSchema;
}
export interface GenerateOptions {
maxNewTokens?: number;
thinking?: boolean;
}ToolExecutor
// agent/types.ts
export interface ToolExecutor {
execute(toolName: string, args: Record<string, unknown>): Promise<unknown>;
}Agent Loop
// agent/loop.ts — simplified illustration
export async function* runAgentLoop(
userMessage: string,
history: ChatMessage[],
model: ModelBackend,
tools: ToolExecutor,
toolDefs: ToolDefinition[],
maxIterations: number
): AsyncGenerator<AgentEvent> {
const messages = [...history, { role: "user", content: userMessage }];
for (let i = 0; i < maxIterations; i++) {
for await (const chunk of model.generate(messages, toolDefs, {})) {
if (chunk.type === "token") yield { type: "token", token: chunk.token };
if (chunk.type === "tool_call") {
yield { type: "tool_start", name: chunk.name };
const result = await tools.execute(chunk.name, chunk.args);
yield { type: "tool_result", name: chunk.name, result };
messages.push({ role: "tool", name: chunk.name, content: String(result) });
}
if (chunk.type === "done") return;
}
}
}Built-in Tools
| Tool | Location | Description |
|---|---|---|
read_page_content | Content script | Read page text/HTML or a CSS selector |
take_screenshot | Service worker | Capture visible tab as PNG |
click_element | Content script | Click by CSS selector |
type_text | Content script | Type into input by CSS selector |
scroll_page | Content script | Scroll by pixel amount |
run_javascript | Service worker | Execute JS in page context |
Adding a New Tool
Tools live in two places: the definition (in the offscreen agent) and the executor (in content script or service worker).
Step 1 — Define the tool schema
// offscreen/tools/definitions.ts
export const MY_TOOL_DEFINITION: ToolDefinition = {
name: "get_page_title",
description: "Returns the document title of the current page.",
parameters: {
type: "object",
properties: {},
required: [],
},
};Step 2 — Register in the tool list
// offscreen/tools/index.ts
import { MY_TOOL_DEFINITION } from "./definitions";
export const ALL_TOOLS: ToolDefinition[] = [
// ...existing tools
MY_TOOL_DEFINITION,
];Step 3 — Implement execution in the content script
// content/tools/executor.ts
export async function executeContentTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "get_page_title":
return document.title;
case "read_page_content": {
const selector = args.selector as string | undefined;
if (selector) {
return document.querySelector(selector)?.textContent ?? "Not found";
}
return document.body.innerText;
}
case "click_element": {
const el = document.querySelector(args.selector as string) as HTMLElement;
if (!el) throw new Error(`Element not found: ${args.selector}`);
el.click();
return "clicked";
}
case "type_text": {
const input = document.querySelector(args.selector as string) as HTMLInputElement;
if (!input) throw new Error(`Input not found: ${args.selector}`);
input.focus();
input.value = args.text as string;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
return "typed";
}
default:
throw new Error(`Unknown content tool: ${name}`);
}
}Step 4 — Handle service-worker-side tools
// background/tools.ts
export async function executeSwTool(
name: string,
args: Record<string, unknown>,
tabId: number
): Promise<unknown> {
switch (name) {
case "take_screenshot": {
const dataUrl = await chrome.tabs.captureVisibleTab({ format: "png" });
return dataUrl;
}
case "run_javascript": {
const results = await chrome.scripting.executeScript({
target: { tabId },
func: new Function(args.code as string) as () => unknown,
});
return results[0]?.result ?? null;
}
default:
return null; // not a SW tool — forward to content script
}
}Message Routing Pattern
The service worker acts as a message bus. All communication uses chrome.runtime.sendMessage.
// Message types (shared/messages.ts)
export type ExtMessage =
| { type: "TOOL_CALL"; name: string; args: Record<string, unknown>; tabId: number }
| { type: "TOOL_RESULT"; name: string; result: unknown }
| { type: "TOKEN"; token: string }
| { type: "AGENT_DONE" }
| { type: "AGENT_ERROR"; error: string };
// Offscreen → SW
chrome.runtime.sendMessage<ExtMessage>({
type: "TOOL_CALL",
name: "click_element",
args: { selector: "#submit-btn" },
tabId: currentTabId,
});
// SW → Content script
chrome.tabs.sendMessage<ExtMessage>(tabId, {
type: "TOOL_CALL",
name: "click_element",
args: { selector: "#submit-btn" },
tabId,
});Model Configuration
// offscreen/model.ts — loading with transformers.js
import { pipeline, TextGenerationPipeline } from "@huggingface/transformers";
const MODEL_IDS = {
E2B: "onnx-community/gemma-4-E2B-it-ONNX",
E4B: "onnx-community/gemma-4-E4B-it-ONNX",
} as const;
export type ModelSize = keyof typeof MODEL_IDS;
export async function loadModel(
size: ModelSize,
onProgress: (progress: number) => void
): Promise<TextGenerationPipeline> {
return pipeline("text-generation", MODEL_IDS[size], {
dtype: "q4f16",
device: "webgpu",
progress_callback: (p: { progress: number }) => onProgress(p.progress),
});
}Settings & Persistence
Settings are stored via chrome.storage.sync:
export interface GemmaGemSettings {More from reason-machines
Use this skill in a project
Open any RiverX project, search the Skills panel for reason-machines/trending-skills/gemma-gem-browser-ai, and install it in one click.
