mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 22:03:45 +00:00
Add custom message extension system with typed renderers and message transformer
- Implement CustomMessages interface for type-safe message extension via declaration merging - Add MessageRenderer<T> with generic typing for custom message rendering - Add messageTransformer to Agent for filtering/transforming messages before LLM - Move message filtering from transports to Agent (separation of concerns) - Add message renderer registry with typed role support - Update web-ui example with SystemNotificationMessage demo - Add custom transformer that converts notifications to <system> tags - Add SessionListDialog onDelete callback for active session cleanup - Handle non-existent session IDs in URL (redirect to new session) - Update both web-ui example and browser extension with session fixes
This commit is contained in:
parent
cf6b3466f8
commit
05dfaa11a8
12 changed files with 457 additions and 152 deletions
107
packages/web-ui/example/src/custom-messages.ts
Normal file
107
packages/web-ui/example/src/custom-messages.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Alert } from "@mariozechner/mini-lit";
|
||||
import type { Message } from "@mariozechner/pi-ai";
|
||||
import { html } from "lit";
|
||||
import { registerMessageRenderer } from "@mariozechner/pi-web-ui";
|
||||
import type { AppMessage, MessageRenderer } from "@mariozechner/pi-web-ui";
|
||||
|
||||
// ============================================================================
|
||||
// 1. EXTEND AppMessage TYPE VIA DECLARATION MERGING
|
||||
// ============================================================================
|
||||
|
||||
// Define custom message types
|
||||
export interface SystemNotificationMessage {
|
||||
role: "system-notification";
|
||||
message: string;
|
||||
variant: "default" | "destructive";
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Extend CustomMessages interface via declaration merging
|
||||
declare module "@mariozechner/pi-web-ui" {
|
||||
interface CustomMessages {
|
||||
"system-notification": SystemNotificationMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 2. CREATE CUSTOM RENDERER (TYPED TO SystemNotificationMessage)
|
||||
// ============================================================================
|
||||
|
||||
const systemNotificationRenderer: MessageRenderer<SystemNotificationMessage> = {
|
||||
render: (notification) => {
|
||||
// notification is fully typed as SystemNotificationMessage!
|
||||
return html`
|
||||
<div class="px-4">
|
||||
${Alert({
|
||||
variant: notification.variant,
|
||||
children: html`
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>${notification.message}</div>
|
||||
<div class="text-xs opacity-70">${new Date(notification.timestamp).toLocaleTimeString()}</div>
|
||||
</div>
|
||||
`,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 3. REGISTER RENDERER
|
||||
// ============================================================================
|
||||
|
||||
export function registerCustomMessageRenderers() {
|
||||
registerMessageRenderer("system-notification", systemNotificationRenderer);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 4. HELPER TO CREATE CUSTOM MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
export function createSystemNotification(
|
||||
message: string,
|
||||
variant: "default" | "destructive" = "default",
|
||||
): SystemNotificationMessage {
|
||||
return {
|
||||
role: "system-notification",
|
||||
message,
|
||||
variant,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 5. CUSTOM MESSAGE TRANSFORMER
|
||||
// ============================================================================
|
||||
|
||||
// Transform custom messages to user messages with <system> tags so LLM can see them
|
||||
export function customMessageTransformer(messages: AppMessage[]): Message[] {
|
||||
return messages
|
||||
.filter((m) => {
|
||||
// Keep LLM-compatible messages + custom messages
|
||||
return (
|
||||
m.role === "user" ||
|
||||
m.role === "assistant" ||
|
||||
m.role === "toolResult" ||
|
||||
m.role === "system-notification"
|
||||
);
|
||||
})
|
||||
.map((m) => {
|
||||
// Transform system notifications to user messages
|
||||
if (m.role === "system-notification") {
|
||||
const notification = m as SystemNotificationMessage;
|
||||
return {
|
||||
role: "user",
|
||||
content: `<system>${notification.message}</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;
|
||||
});
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ import "@mariozechner/mini-lit/dist/ThemeToggle.js";
|
|||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
Agent,
|
||||
AgentState,
|
||||
type AgentState,
|
||||
ApiKeyPromptDialog,
|
||||
ApiKeysTab,
|
||||
type AppMessage,
|
||||
AppStorage,
|
||||
ChatPanel,
|
||||
PersistentStorageDialog,
|
||||
|
|
@ -16,10 +17,13 @@ import {
|
|||
setAppStorage,
|
||||
SettingsDialog,
|
||||
} from "@mariozechner/pi-web-ui";
|
||||
import type { AppMessage } from "@mariozechner/pi-web-ui";
|
||||
import { html, render } from "lit";
|
||||
import { History, Plus, Settings } from "lucide";
|
||||
import { Bell, History, Plus, Settings } from "lucide";
|
||||
import "./app.css";
|
||||
import { createSystemNotification, customMessageTransformer, registerCustomMessageRenderers } from "./custom-messages.js";
|
||||
|
||||
// Register custom message renderers
|
||||
registerCustomMessageRenderers();
|
||||
|
||||
const storage = new AppStorage({
|
||||
sessions: new SessionIndexedDBBackend("pi-web-ui-sessions"),
|
||||
|
|
@ -104,6 +108,8 @@ Feel free to use these tools when needed to provide accurate and helpful respons
|
|||
tools: [],
|
||||
},
|
||||
transport,
|
||||
// Custom transformer: convert system notifications to user messages with <system> tags
|
||||
messageTransformer: customMessageTransformer,
|
||||
});
|
||||
|
||||
agentUnsubscribe = agent.subscribe((event: any) => {
|
||||
|
|
@ -133,13 +139,13 @@ Feel free to use these tools when needed to provide accurate and helpful respons
|
|||
await chatPanel.setAgent(agent);
|
||||
};
|
||||
|
||||
const loadSession = async (sessionId: string) => {
|
||||
if (!storage.sessions) return;
|
||||
const loadSession = async (sessionId: string): Promise<boolean> => {
|
||||
if (!storage.sessions) return false;
|
||||
|
||||
const sessionData = await storage.sessions.loadSession(sessionId);
|
||||
if (!sessionData) {
|
||||
console.error("Session not found:", sessionId);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentSessionId = sessionId;
|
||||
|
|
@ -155,6 +161,7 @@ const loadSession = async (sessionId: string) => {
|
|||
|
||||
updateUrl(sessionId);
|
||||
renderApp();
|
||||
return true;
|
||||
};
|
||||
|
||||
const newSession = () => {
|
||||
|
|
@ -180,9 +187,17 @@ const renderApp = () => {
|
|||
size: "sm",
|
||||
children: icon(History, "sm"),
|
||||
onClick: () => {
|
||||
SessionListDialog.open(async (sessionId) => {
|
||||
await loadSession(sessionId);
|
||||
});
|
||||
SessionListDialog.open(
|
||||
async (sessionId) => {
|
||||
await loadSession(sessionId);
|
||||
},
|
||||
(deletedSessionId) => {
|
||||
// If the deleted session is the current one, start a new session
|
||||
if (deletedSessionId === currentSessionId) {
|
||||
newSession();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
title: "Sessions",
|
||||
})}
|
||||
|
|
@ -246,6 +261,20 @@ const renderApp = () => {
|
|||
: html`<span class="text-base font-semibold text-foreground">Pi Web UI Example</span>`}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
children: icon(Bell, "sm"),
|
||||
onClick: () => {
|
||||
// Demo: Inject custom message
|
||||
if (agent) {
|
||||
agent.appendMessage(
|
||||
createSystemNotification("This is a custom message! It appears in the UI but is never sent to the LLM."),
|
||||
);
|
||||
}
|
||||
},
|
||||
title: "Demo: Add Custom Notification",
|
||||
})}
|
||||
<theme-toggle></theme-toggle>
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
|
|
@ -298,7 +327,12 @@ async function initApp() {
|
|||
const sessionIdFromUrl = urlParams.get("session");
|
||||
|
||||
if (sessionIdFromUrl) {
|
||||
await loadSession(sessionIdFromUrl);
|
||||
const loaded = await loadSession(sessionIdFromUrl);
|
||||
if (!loaded) {
|
||||
// Session doesn't exist, redirect to new session
|
||||
newSession();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await createAgent();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue