mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
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:
parent
05dfaa11a8
commit
c9be21ebad
5 changed files with 137 additions and 0 deletions
32
packages/browser-extension/src/message-transformer.ts
Normal file
32
packages/browser-extension/src/message-transformer.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
68
packages/browser-extension/src/messages/NavigationMessage.ts
Normal file
68
packages/browser-extension/src/messages/NavigationMessage.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue