diff --git a/packages/browser-extension/src/message-transformer.ts b/packages/browser-extension/src/message-transformer.ts new file mode 100644 index 00000000..7ffe15b6 --- /dev/null +++ b/packages/browser-extension/src/message-transformer.ts @@ -0,0 +1,32 @@ +import type { Message } from "@mariozechner/pi-ai"; +import type { AppMessage } from "@mariozechner/pi-web-ui"; +import type { NavigationMessage } from "./messages/NavigationMessage.js"; + +// Custom message transformer for browser extension +// Handles navigation messages and app-specific message types +export function browserMessageTransformer(messages: AppMessage[]): Message[] { + return messages + .filter((m) => { + // Keep LLM-compatible messages + navigation messages + return m.role === "user" || m.role === "assistant" || m.role === "toolResult" || m.role === "navigation"; + }) + .map((m) => { + // Transform navigation messages to user messages with tags + if (m.role === "navigation") { + const nav = m as NavigationMessage; + const tabInfo = nav.tabIndex !== undefined ? ` (tab ${nav.tabIndex})` : ""; + return { + role: "user", + content: `Navigated to ${nav.title}${tabInfo}: ${nav.url}`, + } as Message; + } + + // Strip attachments from user messages + if (m.role === "user") { + const { attachments, ...rest } = m as any; + return rest as Message; + } + + return m as Message; + }); +} diff --git a/packages/browser-extension/src/messages/NavigationMessage.ts b/packages/browser-extension/src/messages/NavigationMessage.ts new file mode 100644 index 00000000..8dbf6b9f --- /dev/null +++ b/packages/browser-extension/src/messages/NavigationMessage.ts @@ -0,0 +1,68 @@ +import type { MessageRenderer } from "@mariozechner/pi-web-ui"; +import { registerMessageRenderer } from "@mariozechner/pi-web-ui"; +import { html } from "lit"; + +// ============================================================================ +// NAVIGATION MESSAGE TYPE +// ============================================================================ + +export interface NavigationMessage { + role: "navigation"; + url: string; + title: string; + favicon?: string; + tabIndex?: number; +} + +// Extend CustomMessages interface via declaration merging +declare module "@mariozechner/pi-web-ui" { + interface CustomMessages { + navigation: NavigationMessage; + } +} + +// ============================================================================ +// RENDERER +// ============================================================================ + +const navigationRenderer: MessageRenderer = { + render: (nav) => { + return html` +
+ ${ + nav.favicon + ? html`` + : html`
` + } + ${nav.title} +
+ `; + }, +}; + +// ============================================================================ +// REGISTER +// ============================================================================ + +export function registerNavigationRenderer() { + registerMessageRenderer("navigation", navigationRenderer); +} + +// ============================================================================ +// HELPER +// ============================================================================ + +export function createNavigationMessage( + url: string, + title: string, + favicon?: string, + tabIndex?: number, +): NavigationMessage { + return { + role: "navigation", + url, + title, + favicon, + tabIndex, + }; +} diff --git a/packages/browser-extension/src/sidepanel.ts b/packages/browser-extension/src/sidepanel.ts index 956b3b3e..532f5ae3 100644 --- a/packages/browser-extension/src/sidepanel.ts +++ b/packages/browser-extension/src/sidepanel.ts @@ -20,9 +20,14 @@ import { } from "@mariozechner/pi-web-ui"; import { html, render } from "lit"; import { History, Plus, Settings } from "lucide"; +import { browserMessageTransformer } from "./message-transformer.js"; +import { createNavigationMessage, registerNavigationRenderer } from "./messages/NavigationMessage.js"; import { browserJavaScriptTool } from "./tools/index.js"; import "./utils/live-reload.js"; +// Register custom message renderers +registerNavigationRenderer(); + declare const browser: any; // Get sandbox URL for extension CSP restrictions @@ -66,6 +71,10 @@ let agent: Agent; let chatPanel: ChatPanel; let agentUnsubscribe: (() => void) | undefined; +// Track last navigation for inserting navigation messages +let lastSubmittedUrl: string | undefined; +let lastSubmittedTabIndex: number | undefined; + // ============================================================================ // HELPERS // ============================================================================ @@ -134,6 +143,7 @@ const createAgent = async (initialState?: Partial) => { tools: [], }, transport, + messageTransformer: browserMessageTransformer, }); agentUnsubscribe = agent.subscribe((event: any) => { @@ -309,6 +319,22 @@ async function initApp() { chatPanel.onApiKeyRequired = async (provider: string) => { return await ApiKeyPromptDialog.prompt(provider); }; + chatPanel.onBeforeSend = async () => { + // Get current tab info + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab || !tab.url) return; + + // Check if navigation changed since last submit + if (lastSubmittedUrl !== tab.url || lastSubmittedTabIndex !== tab.index) { + // Insert navigation message + const navMessage = createNavigationMessage(tab.url, tab.title || "Untitled", tab.favIconUrl, tab.index); + agent.appendMessage(navMessage); + + // Update tracking + lastSubmittedUrl = tab.url; + lastSubmittedTabIndex = tab.index; + } + }; chatPanel.additionalTools = [browserJavaScriptTool]; // Check for session in URL diff --git a/packages/web-ui/src/ChatPanel.ts b/packages/web-ui/src/ChatPanel.ts index cd0bcdf2..c461dd92 100644 --- a/packages/web-ui/src/ChatPanel.ts +++ b/packages/web-ui/src/ChatPanel.ts @@ -22,6 +22,7 @@ export class ChatPanel extends LitElement { @state() private windowWidth = window.innerWidth; @property({ attribute: false }) sandboxUrlProvider?: () => string; @property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise; + @property({ attribute: false }) onBeforeSend?: () => void | Promise; @property({ attribute: false }) additionalTools?: any[]; private resizeHandler = () => { @@ -58,6 +59,7 @@ export class ChatPanel extends LitElement { this.agentInterface.enableThinkingSelector = true; this.agentInterface.showThemeToggle = false; this.agentInterface.onApiKeyRequired = this.onApiKeyRequired; + this.agentInterface.onBeforeSend = this.onBeforeSend; // Create JavaScript REPL tool const javascriptReplTool = createJavaScriptReplTool(); diff --git a/packages/web-ui/src/components/AgentInterface.ts b/packages/web-ui/src/components/AgentInterface.ts index 2998ecc2..3c2acf97 100644 --- a/packages/web-ui/src/components/AgentInterface.ts +++ b/packages/web-ui/src/components/AgentInterface.ts @@ -25,6 +25,10 @@ export class AgentInterface extends LitElement { @property() showThemeToggle = false; // Optional custom API key prompt handler - if not provided, uses default dialog @property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise; + // Optional callback called before sending a message + @property({ attribute: false }) onBeforeSend?: () => void | Promise; + // Optional callback called before executing a tool call - return false to prevent execution + @property({ attribute: false }) onBeforeToolCall?: (toolName: string, args: any) => boolean | Promise; // References @query("message-editor") private _messageEditor!: MessageEditor; @@ -186,6 +190,11 @@ export class AgentInterface extends LitElement { } } + // Call onBeforeSend hook before sending + if (this.onBeforeSend) { + await this.onBeforeSend(); + } + // Only clear editor after we know we can send this._messageEditor.value = ""; this._messageEditor.attachments = [];