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:
Mario Zechner 2025-10-06 13:45:08 +02:00
parent cf6b3466f8
commit 05dfaa11a8
12 changed files with 457 additions and 152 deletions

View file

@ -2,15 +2,16 @@ import { html } from "@mariozechner/mini-lit";
import type {
AgentTool,
AssistantMessage as AssistantMessageType,
Message,
ToolResultMessage as ToolResultMessageType,
} from "@mariozechner/pi-ai";
import { LitElement, type TemplateResult } from "lit";
import { property } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import type { AppMessage } from "./Messages.js";
import { renderMessage } from "./message-renderer-registry.js";
export class MessageList extends LitElement {
@property({ type: Array }) messages: Message[] = [];
@property({ type: Array }) messages: AppMessage[] = [];
@property({ type: Array }) tools: AgentTool[] = [];
@property({ type: Object }) pendingToolCalls?: Set<string>;
@property({ type: Boolean }) isStreaming: boolean = false;
@ -36,6 +37,15 @@ export class MessageList extends LitElement {
const items: Array<{ key: string; template: TemplateResult }> = [];
let index = 0;
for (const msg of this.messages) {
// Try custom renderer first
const customTemplate = renderMessage(msg);
if (customTemplate) {
items.push({ key: `msg:${index}`, template: customTemplate });
index++;
continue;
}
// Fall back to built-in renderers
if (msg.role === "user") {
items.push({
key: `msg:${index}`,
@ -58,7 +68,7 @@ export class MessageList extends LitElement {
index++;
} else {
// Skip standalone toolResult messages; they are rendered via paired tool-message above
// For completeness, other roles are not expected
// Skip unknown roles
}
}
return items;

View file

@ -16,7 +16,23 @@ import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
export type UserMessageWithAttachments = UserMessageType & { attachments?: Attachment[] };
export type AppMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType;
// Base message union
type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType;
// Extensible interface - apps can extend via declaration merging
// Example:
// declare module "@mariozechner/pi-web-ui" {
// interface CustomMessages {
// "system-notification": SystemNotificationMessage;
// }
// }
export interface CustomMessages {
// Empty by default - apps extend via declaration merging
}
// AppMessage is union of base messages + custom messages
export type AppMessage = BaseMessage | CustomMessages[keyof CustomMessages];
@customElement("user-message")
export class UserMessage extends LitElement {

View file

@ -0,0 +1,28 @@
import type { TemplateResult } from "lit";
import type { AppMessage } from "./Messages.js";
// Extract role type from AppMessage union
export type MessageRole = AppMessage["role"];
// Generic message renderer typed to specific message type
export interface MessageRenderer<TMessage extends AppMessage = AppMessage> {
render(message: TMessage): TemplateResult;
}
// Registry of custom message renderers by role
const messageRenderers = new Map<MessageRole, MessageRenderer<any>>();
export function registerMessageRenderer<TRole extends MessageRole>(
role: TRole,
renderer: MessageRenderer<Extract<AppMessage, { role: TRole }>>,
): void {
messageRenderers.set(role, renderer);
}
export function getMessageRenderer(role: MessageRole): MessageRenderer | undefined {
return messageRenderers.get(role);
}
export function renderMessage(message: AppMessage): TemplateResult | undefined {
return messageRenderers.get(message.role)?.render(message);
}