mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 00:04:50 +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";
|
} from "@mariozechner/pi-web-ui";
|
||||||
import { html, render } from "lit";
|
import { html, render } from "lit";
|
||||||
import { History, Plus, Settings } from "lucide";
|
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 { browserJavaScriptTool } from "./tools/index.js";
|
||||||
import "./utils/live-reload.js";
|
import "./utils/live-reload.js";
|
||||||
|
|
||||||
|
// Register custom message renderers
|
||||||
|
registerNavigationRenderer();
|
||||||
|
|
||||||
declare const browser: any;
|
declare const browser: any;
|
||||||
|
|
||||||
// Get sandbox URL for extension CSP restrictions
|
// Get sandbox URL for extension CSP restrictions
|
||||||
|
|
@ -66,6 +71,10 @@ let agent: Agent;
|
||||||
let chatPanel: ChatPanel;
|
let chatPanel: ChatPanel;
|
||||||
let agentUnsubscribe: (() => void) | undefined;
|
let agentUnsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
|
// Track last navigation for inserting navigation messages
|
||||||
|
let lastSubmittedUrl: string | undefined;
|
||||||
|
let lastSubmittedTabIndex: number | undefined;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// HELPERS
|
// HELPERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -134,6 +143,7 @@ const createAgent = async (initialState?: Partial<AgentState>) => {
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
transport,
|
transport,
|
||||||
|
messageTransformer: browserMessageTransformer,
|
||||||
});
|
});
|
||||||
|
|
||||||
agentUnsubscribe = agent.subscribe((event: any) => {
|
agentUnsubscribe = agent.subscribe((event: any) => {
|
||||||
|
|
@ -309,6 +319,22 @@ async function initApp() {
|
||||||
chatPanel.onApiKeyRequired = async (provider: string) => {
|
chatPanel.onApiKeyRequired = async (provider: string) => {
|
||||||
return await ApiKeyPromptDialog.prompt(provider);
|
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];
|
chatPanel.additionalTools = [browserJavaScriptTool];
|
||||||
|
|
||||||
// Check for session in URL
|
// Check for session in URL
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export class ChatPanel extends LitElement {
|
||||||
@state() private windowWidth = window.innerWidth;
|
@state() private windowWidth = window.innerWidth;
|
||||||
@property({ attribute: false }) sandboxUrlProvider?: () => string;
|
@property({ attribute: false }) sandboxUrlProvider?: () => string;
|
||||||
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
||||||
|
@property({ attribute: false }) onBeforeSend?: () => void | Promise<void>;
|
||||||
@property({ attribute: false }) additionalTools?: any[];
|
@property({ attribute: false }) additionalTools?: any[];
|
||||||
|
|
||||||
private resizeHandler = () => {
|
private resizeHandler = () => {
|
||||||
|
|
@ -58,6 +59,7 @@ export class ChatPanel extends LitElement {
|
||||||
this.agentInterface.enableThinkingSelector = true;
|
this.agentInterface.enableThinkingSelector = true;
|
||||||
this.agentInterface.showThemeToggle = false;
|
this.agentInterface.showThemeToggle = false;
|
||||||
this.agentInterface.onApiKeyRequired = this.onApiKeyRequired;
|
this.agentInterface.onApiKeyRequired = this.onApiKeyRequired;
|
||||||
|
this.agentInterface.onBeforeSend = this.onBeforeSend;
|
||||||
|
|
||||||
// Create JavaScript REPL tool
|
// Create JavaScript REPL tool
|
||||||
const javascriptReplTool = createJavaScriptReplTool();
|
const javascriptReplTool = createJavaScriptReplTool();
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ export class AgentInterface extends LitElement {
|
||||||
@property() showThemeToggle = false;
|
@property() showThemeToggle = false;
|
||||||
// Optional custom API key prompt handler - if not provided, uses default dialog
|
// Optional custom API key prompt handler - if not provided, uses default dialog
|
||||||
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
@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
|
// References
|
||||||
@query("message-editor") private _messageEditor!: MessageEditor;
|
@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
|
// Only clear editor after we know we can send
|
||||||
this._messageEditor.value = "";
|
this._messageEditor.value = "";
|
||||||
this._messageEditor.attachments = [];
|
this._messageEditor.attachments = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue