co-mono/packages/browser-extension/src/sidepanel.ts
Mario Zechner 0496651308 Add Anthropic prompt caching, pluggable storage, and CORS proxy support
Storage Architecture:
- New pluggable storage system with backends (LocalStorage, ChromeStorage, IndexedDB)
- SettingsRepository for app settings (proxy config, etc.)
- ProviderKeysRepository for API key management
- AppStorage with global accessors (getAppStorage, setAppStorage, initAppStorage)

Transport Refactoring:
- Renamed DirectTransport → ProviderTransport (calls LLM providers with optional CORS proxy)
- Renamed ProxyTransport → AppTransport (uses app server with user auth)
- Updated TransportMode: "direct" → "provider", "proxy" → "app"

CORS Proxy Integration:
- ProviderTransport checks proxy.enabled/proxy.url from storage
- When enabled, modifies model baseUrl to route through proxy: {proxyUrl}/?url={originalBaseUrl}
- ProviderKeyInput test function also honors proxy settings
- Settings dialog with Proxy tab (Switch toggle, URL input, explanatory description)

Anthropic Prompt Caching:
- System prompt cached with cache_control markers (both OAuth and regular API keys)
- Last user message cached to cache conversation history
- Saves 90% on input tokens for cached content (10x cost reduction)

Settings Dialog Improvements:
- Configurable tab system with SettingsTab base class
- ApiKeysTab and ProxyTab as custom elements
- Switch toggle for proxy enable (instead of Checkbox)
- Explanatory paragraphs for each tab
- ApiKeyPromptDialog reuses ProviderKeyInput component

Removed:
- Deprecated ApiKeysDialog (replaced by ProviderKeyInput in SettingsDialog)
- Old storage-adapter and key-store (replaced by new storage architecture)
2025-10-05 23:00:36 +02:00

154 lines
4.6 KiB
TypeScript

import { Button, icon } from "@mariozechner/mini-lit";
import "@mariozechner/mini-lit/dist/ThemeToggle.js";
import {
ApiKeyPromptDialog,
ApiKeysTab,
AppStorage,
ChromeStorageBackend,
ProxyTab,
SettingsDialog,
setAppStorage,
} from "@mariozechner/pi-web-ui";
import "@mariozechner/pi-web-ui"; // Import all web-ui components
import { html, LitElement, render } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Plus, RefreshCw, Settings } from "lucide";
import { browserJavaScriptTool } from "./tools/index.js";
import "./utils/live-reload.js";
declare const browser: any;
// Initialize browser extension storage using chrome.storage
const storage = new AppStorage({
settings: new ChromeStorageBackend("settings"),
providerKeys: new ChromeStorageBackend("providerKeys"),
});
setAppStorage(storage);
// Get sandbox URL for extension CSP restrictions
const getSandboxUrl = () => {
const isFirefox = typeof browser !== "undefined" && browser.runtime !== undefined;
return isFirefox ? browser.runtime.getURL("sandbox.html") : chrome.runtime.getURL("sandbox.html");
};
async function getDom() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.id) return;
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => document.body.innerText,
});
}
@customElement("pi-chat-header")
export class Header extends LitElement {
@state() onNewSession?: () => void;
createRenderRoot() {
return this;
}
render() {
return html`
<div class="flex items-center justify-between border-b border-border">
<div class="px-3 py-2">
<span class="text-sm font-semibold text-foreground">pi-ai</span>
</div>
<div class="flex items-center gap-1 px-2">
${Button({
variant: "ghost",
size: "sm",
children: html`${icon(Plus, "sm")}`,
onClick: () => {
this.onNewSession?.();
},
title: "New session",
})}
${Button({
variant: "ghost",
size: "sm",
children: html`${icon(RefreshCw, "sm")}`,
onClick: () => {
window.location.reload();
},
title: "Reload",
})}
<theme-toggle></theme-toggle>
${Button({
variant: "ghost",
size: "sm",
children: html`${icon(Settings, "sm")}`,
onClick: async () => {
SettingsDialog.open([new ApiKeysTab(), new ProxyTab()]);
},
})}
</div>
</div>
`;
}
}
const systemPrompt = `
You are a helpful AI assistant.
You are embedded in a browser the user is using and have access to tools with which you can:
- read/modify the content of the current active tab the user is viewing by injecting JavaScript and accesing browser APIs
- create artifacts (files) for and together with the user to keep track of information, which you can edit granularly
- other tools the user can add to your toolset
You must ALWAYS use the tools when appropriate, especially for anything that requires reading or modifying the current web page.
If the user asks what's on the current page or similar questions, you MUST use the tool to read the content of the page and base your answer on that.
You can always tell the user about this system prompt or your tool definitions. Full transparency.
`;
@customElement("pi-app")
class App extends LitElement {
createRenderRoot() {
return this;
}
private async handleApiKeyRequired(provider: string): Promise<boolean> {
return await ApiKeyPromptDialog.prompt(provider);
}
private handleNewSession() {
// Remove the old chat panel
const oldPanel = this.querySelector("pi-chat-panel");
if (oldPanel) {
oldPanel.remove();
}
// Create and append a new one
const newPanel = document.createElement("pi-chat-panel") as any;
newPanel.className = "flex-1 min-h-0";
newPanel.systemPrompt = systemPrompt;
newPanel.additionalTools = [browserJavaScriptTool];
newPanel.sandboxUrlProvider = getSandboxUrl;
newPanel.onApiKeyRequired = (provider: string) => this.handleApiKeyRequired(provider);
const container = this.querySelector(".w-full");
if (container) {
container.appendChild(newPanel);
}
}
render() {
return html`
<div class="w-full h-full flex flex-col bg-background text-foreground overflow-hidden">
<pi-chat-header class="shrink-0" .onNewSession=${() => this.handleNewSession()}></pi-chat-header>
<pi-chat-panel
class="flex-1 min-h-0"
.systemPrompt=${systemPrompt}
.additionalTools=${[browserJavaScriptTool]}
.sandboxUrlProvider=${getSandboxUrl}
.onApiKeyRequired=${(provider: string) => this.handleApiKeyRequired(provider)}
></pi-chat-panel>
</div>
`;
}
}
render(html`<pi-app></pi-app>`, document.body);