Add navigation message tracking to browser extension

- Add onBeforeSend callback to ChatPanel and AgentInterface
- Add onBeforeToolCall callback (for future permission dialogs)
- Create NavigationMessage custom message type
- Add browserMessageTransformer that converts nav messages to <system> tags
- Track last submitted URL and tab index
- Auto-insert navigation message when URL/tab changes on user submit
- Navigation message shows favicon + page title in UI
- LLM receives: <system>Navigated to [title] (tab X): [url]</system>
This commit is contained in:
Mario Zechner 2025-10-06 13:49:28 +02:00
parent 05dfaa11a8
commit c9be21ebad
5 changed files with 137 additions and 0 deletions

View file

@ -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 <system> tags
if (m.role === "navigation") {
const nav = m as NavigationMessage;
const tabInfo = nav.tabIndex !== undefined ? ` (tab ${nav.tabIndex})` : "";
return {
role: "user",
content: `<system>Navigated to ${nav.title}${tabInfo}: ${nav.url}</system>`,
} 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;
});
}

View file

@ -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<NavigationMessage> = {
render: (nav) => {
return html`
<div class="flex items-center gap-2 px-4 py-2 text-sm text-muted-foreground">
${
nav.favicon
? html`<img src="${nav.favicon}" alt="" class="w-4 h-4 flex-shrink-0" />`
: html`<div class="w-4 h-4 flex-shrink-0 bg-muted rounded"></div>`
}
<span class="truncate">${nav.title}</span>
</div>
`;
},
};
// ============================================================================
// 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,
};
}

View file

@ -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<AgentState>) => {
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

View file

@ -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<boolean>;
@property({ attribute: false }) onBeforeSend?: () => void | Promise<void>;
@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();

View file

@ -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<boolean>;
// Optional callback called before sending a message
@property({ attribute: false }) onBeforeSend?: () => void | Promise<void>;
// Optional callback called before executing a tool call - return false to prevent execution
@property({ attribute: false }) onBeforeToolCall?: (toolName: string, args: any) => boolean | Promise<boolean>;
// 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 = [];