From 3266d81b63cd65cb3fc9bb314dbe81cace8d3ced Mon Sep 17 00:00:00 2001 From: Vincent Yung Date: Wed, 12 Nov 2025 11:45:48 -0800 Subject: [PATCH 001/126] feat: Rebrand to GoDaddy ANS and add multi-provider support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rebrand from Atlas to GoDaddy ANS Chat Sidebar - Add support for Anthropic Claude and OpenAI providers - Implement browser automation tools for Anthropic (navigate, click, type, screenshot) - Add custom base URL and model name configuration - Implement per-tab isolated chat history - Add automatic page context extraction - Fix auto-scroll during streaming responses - Update header to show correct provider/model ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- REVERT_BROWSER_TOOLS.md | 139 ++++++++ anthropic-browser-tools.ts | 201 ++++++++++++ anthropic-service.ts | 68 ++++ background.ts | 9 +- .../src/renderer/components/Settings.tsx | 26 ++ .../src/renderer/services/gemini-service.ts | 3 +- electron-browser/src/renderer/types.ts | 2 + manifest.json | 6 +- settings.html | 2 +- settings.tsx | 90 +++++- sidepanel.html | 2 +- sidepanel.tsx | 299 ++++++++++++++---- types.ts | 5 +- 13 files changed, 769 insertions(+), 83 deletions(-) create mode 100644 REVERT_BROWSER_TOOLS.md create mode 100644 anthropic-browser-tools.ts create mode 100644 anthropic-service.ts diff --git a/REVERT_BROWSER_TOOLS.md b/REVERT_BROWSER_TOOLS.md new file mode 100644 index 0000000..ec0d993 --- /dev/null +++ b/REVERT_BROWSER_TOOLS.md @@ -0,0 +1,139 @@ +# How to Revert Anthropic Browser Tools + +If you want to remove the Anthropic Browser Tools feature and go back to the simple chat-only version, follow these steps: + +## Option 1: Quick Revert (Just disable the feature) + +**In the UI:** +1. Open the Atlas sidebar +2. Click the **โ—‰** button to disable Browser Tools mode (it will turn to **โ—‹**) +3. Anthropic will work in simple chat mode (no navigation/browser control) + +This is the **easiest way** - no code changes needed! + +--- + +## Option 2: Full Revert (Remove the code) + +If you want to completely remove the Anthropic Browser Tools code: + +### Files to Delete: +```bash +rm anthropic-browser-tools.ts +``` + +### Files to Modify: + +**1. `sidepanel.tsx` (Line 9)** + +Remove this import: +```typescript +import { streamAnthropicWithBrowserTools } from './anthropic-browser-tools'; +``` + +**2. `sidepanel.tsx` (Lines 1259-1297)** + +Replace this block: +```typescript + // Route to provider-specific browser tools + if (settings.provider === 'google') { + await streamWithGeminiComputerUse(newMessages); + } else if (settings.provider === 'anthropic') { + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: '', + }; + setMessages(prev => [...prev, assistantMessage]); + + const modelToUse = settings.model === 'custom' && settings.customModelName + ? settings.customModelName + : settings.model; + + await streamAnthropicWithBrowserTools( + newMessages, + settings.apiKey, + modelToUse, + settings.customBaseUrl, + (text: string) => { + setMessages(prev => { + const updated = [...prev]; + const lastMsg = updated[updated.length - 1]; + if (lastMsg && lastMsg.role === 'assistant') { + lastMsg.content += text; + } + return updated; + }); + }, + () => { + // On complete + }, + executeTool, + abortControllerRef.current.signal + ); + } else { + throw new Error(`Browser Tools not supported for ${settings.provider}`); + } +``` + +With this simpler version: +```typescript + // Safety check: Ensure we have Google API key + if (settings.provider !== 'google' || !settings.apiKey) { + setBrowserToolsEnabled(false); + setMessages(prev => { + const updated = [...prev]; + const lastMsg = updated[updated.length - 1]; + if (lastMsg && lastMsg.role === 'assistant') { + lastMsg.content = 'โš ๏ธ **Browser Tools requires Google Gemini**\n\nBrowser Tools only works with Google Gemini.\n\nPlease:\n1. Open Settings (โš™๏ธ)\n2. Select "Google" as provider\n3. Add your Google API key\n4. Try again'; + } + return updated; + }); + setIsLoading(false); + return; + } + + await streamWithGeminiComputerUse(newMessages); +``` + +### Rebuild: +```bash +npm run build +``` + +### Reload extension: +Go to `chrome://extensions/` and click the refresh icon on the Atlas extension. + +--- + +## What Changed (Summary) + +### New Files Added: +- โœ… `anthropic-browser-tools.ts` - Anthropic-specific browser automation + +### Modified Files: +- โœ… `sidepanel.tsx` - Added routing for Anthropic browser tools in Browser Tools mode + +### What Stayed the Same: +- โœ… Simple chat mode (without Browser Tools button) works exactly as before +- โœ… Google Gemini Computer Use - unchanged +- โœ… All settings and configuration - unchanged +- โœ… Composio MCP integration - unchanged + +--- + +## Testing the Revert + +After reverting, test that: +1. โœ… Anthropic simple chat still works (without Browser Tools) +2. โœ… Google Gemini Computer Use still works with Browser Tools +3. โœ… No errors in console + +--- + +## Contact + +If you have issues reverting, you can: +1. Check the git history: `git log` +2. Revert to a previous commit: `git revert ` +3. Or restore from backup (if you made one before changes) diff --git a/anthropic-browser-tools.ts b/anthropic-browser-tools.ts new file mode 100644 index 0000000..712dccf --- /dev/null +++ b/anthropic-browser-tools.ts @@ -0,0 +1,201 @@ +import type { Message } from './types'; + +// Browser tool definitions for Anthropic API +const BROWSER_TOOLS = [ + { + name: 'navigate', + description: 'Navigate to a specific URL', + input_schema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'The URL to navigate to (must include http:// or https://)', + }, + }, + required: ['url'], + }, + }, + { + name: 'click', + description: 'Click at specific coordinates on the page', + input_schema: { + type: 'object', + properties: { + x: { type: 'number', description: 'X coordinate' }, + y: { type: 'number', description: 'Y coordinate' }, + }, + required: ['x', 'y'], + }, + }, + { + name: 'type', + description: 'Type text into a focused input field', + input_schema: { + type: 'object', + properties: { + text: { type: 'string', description: 'Text to type' }, + selector: { type: 'string', description: 'CSS selector for the input (optional)' }, + }, + required: ['text'], + }, + }, + { + name: 'scroll', + description: 'Scroll the page', + input_schema: { + type: 'object', + properties: { + direction: { + type: 'string', + enum: ['up', 'down'], + description: 'Scroll direction', + }, + amount: { type: 'number', description: 'Pixels to scroll (default: 500)' }, + }, + required: ['direction'], + }, + }, + { + name: 'screenshot', + description: 'Take a screenshot of the current page', + input_schema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'getPageContext', + description: 'Get information about the current page (URL, title, content)', + input_schema: { + type: 'object', + properties: {}, + }, + }, +]; + +export async function streamAnthropicWithBrowserTools( + messages: Message[], + apiKey: string, + model: string, + customBaseUrl: string | undefined, + onTextChunk: (text: string) => void, + onComplete: () => void, + executeTool: (toolName: string, params: any) => Promise, + signal?: AbortSignal +): Promise { + const baseUrl = customBaseUrl || 'https://api.anthropic.com'; + let conversationMessages = [...messages]; + let fullResponseText = ''; + + const MAX_TURNS = 10; // Prevent infinite loops + let turnCount = 0; + + while (turnCount < MAX_TURNS) { + turnCount++; + + console.log('๐Ÿ”ง Anthropic Browser Tools - Turn', turnCount); + console.log('๐Ÿ“ค Sending request with tools:', BROWSER_TOOLS.map(t => t.name)); + + const requestBody = { + model, + max_tokens: 4096, + tools: BROWSER_TOOLS, + messages: conversationMessages.map(m => ({ + role: m.role, + content: m.content, + })), + system: 'You are a helpful AI assistant with browser automation capabilities. You can navigate to websites, click elements, type text, scroll pages, and take screenshots. When the user asks you to visit a website or interact with a page, USE THE AVAILABLE TOOLS to perform the action.', + }; + + console.log('๐Ÿ“ค Request body:', JSON.stringify(requestBody, null, 2)); + + const response = await fetch(`${baseUrl}/v1/messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify(requestBody), + signal, + }); + + if (!response.ok) { + const error = await response.json(); + console.error('โŒ API Error:', error); + throw new Error(error.error?.message || 'Anthropic API request failed'); + } + + const data = await response.json(); + console.log('๐Ÿ“ฅ Response:', JSON.stringify(data, null, 2)); + + // Check if response includes text + const textContent = data.content?.find((c: any) => c.type === 'text'); + if (textContent?.text) { + fullResponseText += textContent.text; + onTextChunk(textContent.text); + } + + // Check for tool use + const toolUses = data.content?.filter((c: any) => c.type === 'tool_use') || []; + + if (toolUses.length === 0) { + // No more tools to execute, we're done + break; + } + + // Execute tools and collect results + const toolResults: any[] = []; + + for (const toolUse of toolUses) { + console.log(`๐Ÿ”ง Executing tool: ${toolUse.name}`, toolUse.input); + onTextChunk(`\n[Executing: ${toolUse.name}]\n`); + onTextChunk(`\n${JSON.stringify(toolUse.input, null, 2)}\n`); + + try { + console.log('๐Ÿ”ง Calling executeTool with:', toolUse.name, toolUse.input); + const result = await executeTool(toolUse.name, toolUse.input); + console.log('โœ… Tool result:', result); + + toolResults.push({ + type: 'tool_result', + tool_use_id: toolUse.id, + content: JSON.stringify(result), + }); + + // Small delay between actions + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (error: any) { + console.error('โŒ Tool execution error:', error); + toolResults.push({ + type: 'tool_result', + tool_use_id: toolUse.id, + content: JSON.stringify({ error: error.message }), + is_error: true, + }); + } + } + + // Add assistant message with tool uses + conversationMessages.push({ + id: Date.now().toString(), + role: 'assistant', + content: JSON.stringify(data.content), + }); + + // Add user message with tool results + conversationMessages.push({ + id: (Date.now() + 1).toString(), + role: 'user', + content: JSON.stringify(toolResults), + }); + + // If the response has a stop_reason of 'end_turn', we're done + if (data.stop_reason === 'end_turn') { + break; + } + } + + onComplete(); +} diff --git a/anthropic-service.ts b/anthropic-service.ts new file mode 100644 index 0000000..e707ad6 --- /dev/null +++ b/anthropic-service.ts @@ -0,0 +1,68 @@ +import type { Message } from './types'; + +export async function streamAnthropic( + messages: Message[], + apiKey: string, + model: string, + customBaseUrl: string | undefined, + onChunk: (text: string) => void, + signal?: AbortSignal +): Promise { + const baseUrl = customBaseUrl || 'https://api.anthropic.com'; + + const response = await fetch(`${baseUrl}/v1/messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model, + max_tokens: 4096, + messages: messages.map(m => ({ + role: m.role, + content: m.content, + })), + stream: true, + }), + signal, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error?.message || 'Anthropic API request failed'); + } + + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim() || !line.startsWith('data: ')) continue; + + const data = line.slice(6); // Remove 'data: ' prefix + if (data === '[DONE]') continue; + + try { + const json = JSON.parse(data); + + if (json.type === 'content_block_delta' && json.delta?.type === 'text_delta') { + onChunk(json.delta.text); + } + } catch (e) { + // Skip invalid JSON + } + } + } +} diff --git a/background.ts b/background.ts index 0cf9ead..87bb73b 100644 --- a/background.ts +++ b/background.ts @@ -13,17 +13,12 @@ const memory: BrowserMemory = { sessionData: {} }; +// Set side panel to open automatically on extension icon click +// The side panel will be per-tab by default when using tabId chrome.sidePanel .setPanelBehavior({ openPanelOnActionClick: true }) .catch((error: Error) => console.error(error)); -// Listen for extension icon clicks -chrome.action.onClicked.addListener((tab) => { - if (tab.id) { - chrome.sidePanel.open({ tabId: tab.id }); - } -}); - // Track page visits for memory chrome.webNavigation.onCompleted.addListener((details) => { if (details.frameId === 0) { // Main frame only diff --git a/electron-browser/src/renderer/components/Settings.tsx b/electron-browser/src/renderer/components/Settings.tsx index 82d918c..4cf6127 100644 --- a/electron-browser/src/renderer/components/Settings.tsx +++ b/electron-browser/src/renderer/components/Settings.tsx @@ -12,6 +12,7 @@ const Settings = ({ settings, onSave, onClose }: SettingsProps) => { const [googleApiKey, setGoogleApiKey] = useState(settings?.googleApiKey || ''); const [composioApiKey, setComposioApiKey] = useState(settings?.composioApiKey || ''); const [model, setModel] = useState(settings?.model || 'gemini-2.0-flash-exp'); + const [customBaseUrl, setCustomBaseUrl] = useState(settings?.customBaseUrl || ''); const handleSave = async () => { // Initialize MCP if Composio key is provided @@ -29,6 +30,7 @@ const Settings = ({ settings, onSave, onClose }: SettingsProps) => { googleApiKey, composioApiKey, model, + customBaseUrl, }); }; @@ -112,6 +114,30 @@ const Settings = ({ settings, onSave, onClose }: SettingsProps) => {

+ {/* Custom Base URL */} +
+ + setCustomBaseUrl(e.target.value)} + placeholder="e.g., https://your-custom-endpoint.com" + style={{ + padding: '10px 12px', + backgroundColor: '#f5f5f5', + color: '#1a1a1a', + border: '1px solid #e5e5e5', + borderRadius: '6px', + fontSize: '14px', + }} + /> +

+ Leave empty to use default Google AI endpoint. Enter a custom Gemini-compatible API endpoint to use your own provider. +

+
+ {/* Composio API Key */}