import { Button, Input, icon } from "@mariozechner/mini-lit"; import "@mariozechner/mini-lit/dist/ThemeToggle.js"; import { getModel } from "@mariozechner/pi-ai"; import { Agent, type AgentState, ApiKeyPromptDialog, ApiKeysTab, type AppMessage, AppStorage, ChatPanel, ChromeStorageBackend, PersistentStorageDialog, ProviderTransport, ProxyTab, SessionIndexedDBBackend, SessionListDialog, SettingsDialog, setAppStorage, } from "@mariozechner/pi-web-ui"; import { html, render } from "lit"; import { History, Plus, Settings } from "lucide"; import { browserJavaScriptTool } from "./tools/index.js"; import "./utils/live-reload.js"; declare const browser: any; // 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"); }; 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. `; // ============================================================================ // STORAGE SETUP // ============================================================================ const storage = new AppStorage({ settings: new ChromeStorageBackend("settings"), providerKeys: new ChromeStorageBackend("providerKeys"), sessions: new SessionIndexedDBBackend("pi-extension-sessions"), }); setAppStorage(storage); // ============================================================================ // APP STATE // ============================================================================ let currentSessionId: string | undefined; let currentTitle = ""; let isEditingTitle = false; let agent: Agent; let chatPanel: ChatPanel; let agentUnsubscribe: (() => void) | undefined; // ============================================================================ // HELPERS // ============================================================================ const generateTitle = (messages: AppMessage[]): string => { const firstUserMsg = messages.find((m) => m.role === "user"); if (!firstUserMsg || firstUserMsg.role !== "user") return ""; let text = ""; const content = firstUserMsg.content; if (typeof content === "string") { text = content; } else { const textBlocks = content.filter((c: any) => c.type === "text"); text = textBlocks.map((c: any) => c.text || "").join(" "); } text = text.trim(); if (!text) return ""; const sentenceEnd = text.search(/[.!?]/); if (sentenceEnd > 0 && sentenceEnd <= 50) { return text.substring(0, sentenceEnd + 1); } return text.length <= 50 ? text : text.substring(0, 47) + "..."; }; const shouldSaveSession = (messages: AppMessage[]): boolean => { const hasUserMsg = messages.some((m: any) => m.role === "user"); const hasAssistantMsg = messages.some((m: any) => m.role === "assistant"); return hasUserMsg && hasAssistantMsg; }; const saveSession = async () => { if (!storage.sessions || !currentSessionId || !agent || !currentTitle) return; const state = agent.state; if (!shouldSaveSession(state.messages)) return; try { await storage.sessions.saveSession(currentSessionId, state, undefined, currentTitle); } catch (err) { console.error("Failed to save session:", err); } }; const updateUrl = (sessionId: string) => { const url = new URL(window.location.href); url.searchParams.set("session", sessionId); window.history.replaceState({}, "", url); }; const createAgent = async (initialState?: Partial) => { if (agentUnsubscribe) { agentUnsubscribe(); } const transport = new ProviderTransport(); agent = new Agent({ initialState: initialState || { systemPrompt, model: getModel("anthropic", "claude-sonnet-4-5-20250929"), thinkingLevel: "off", messages: [], tools: [], }, transport, }); agentUnsubscribe = agent.subscribe((event: any) => { if (event.type === "state-update") { const messages = event.state.messages; // Generate title after first successful response if (!currentTitle && shouldSaveSession(messages)) { currentTitle = generateTitle(messages); } // Create session ID on first successful save if (!currentSessionId && shouldSaveSession(messages)) { currentSessionId = crypto.randomUUID(); updateUrl(currentSessionId); } // Auto-save if (currentSessionId) { saveSession(); } renderApp(); } }); await chatPanel.setAgent(agent); }; const loadSession = (sessionId: string) => { const url = new URL(window.location.href); url.searchParams.set("session", sessionId); window.location.href = url.toString(); }; const newSession = () => { const url = new URL(window.location.href); url.search = ""; window.location.href = url.toString(); }; // ============================================================================ // RENDER // ============================================================================ const renderApp = () => { const appHtml = html`
${Button({ variant: "ghost", size: "sm", children: icon(History, "sm"), onClick: () => { SessionListDialog.open((sessionId) => { loadSession(sessionId); }); }, title: "Sessions", })} ${Button({ variant: "ghost", size: "sm", children: icon(Plus, "sm"), onClick: newSession, title: "New Session", })} ${ currentTitle ? isEditingTitle ? html`
${Input({ type: "text", value: currentTitle, className: "text-sm w-48", onChange: async (e: Event) => { const newTitle = (e.target as HTMLInputElement).value.trim(); if (newTitle && newTitle !== currentTitle && storage.sessions && currentSessionId) { await storage.sessions.updateTitle(currentSessionId, newTitle); currentTitle = newTitle; } isEditingTitle = false; renderApp(); }, onKeyDown: async (e: KeyboardEvent) => { if (e.key === "Enter") { const newTitle = (e.target as HTMLInputElement).value.trim(); if (newTitle && newTitle !== currentTitle && storage.sessions && currentSessionId) { await storage.sessions.updateTitle(currentSessionId, newTitle); currentTitle = newTitle; } isEditingTitle = false; renderApp(); } else if (e.key === "Escape") { isEditingTitle = false; renderApp(); } }, })}
` : html`` : html`pi-ai` }
${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"), onClick: () => SettingsDialog.open([new ApiKeysTab(), new ProxyTab()]), title: "Settings", })}
${chatPanel}
`; render(appHtml, document.body); }; // ============================================================================ // INIT // ============================================================================ async function initApp() { // Show loading render( html`
Loading...
`, document.body, ); // Request persistent storage if (storage.sessions) { await PersistentStorageDialog.request(); } // Create ChatPanel chatPanel = new ChatPanel(); chatPanel.sandboxUrlProvider = getSandboxUrl; chatPanel.onApiKeyRequired = async (provider: string) => { return await ApiKeyPromptDialog.prompt(provider); }; chatPanel.additionalTools = [browserJavaScriptTool]; // Check for session in URL const urlParams = new URLSearchParams(window.location.search); let sessionIdFromUrl = urlParams.get("session"); // If no session in URL, try to load the most recent session if (!sessionIdFromUrl && storage.sessions) { const latestSessionId = await storage.sessions.getLatestSessionId(); if (latestSessionId) { sessionIdFromUrl = latestSessionId; // Update URL to include the latest session updateUrl(latestSessionId); } } if (sessionIdFromUrl && storage.sessions) { const sessionData = await storage.sessions.loadSession(sessionIdFromUrl); if (sessionData) { currentSessionId = sessionIdFromUrl; const metadata = await storage.sessions.getMetadata(sessionIdFromUrl); currentTitle = metadata?.title || ""; await createAgent({ systemPrompt, model: sessionData.model, thinkingLevel: sessionData.thinkingLevel, messages: sessionData.messages, tools: [], }); renderApp(); return; } } // No session or session not found - create new agent await createAgent(); renderApp(); } initApp();