Files
pi_harness/packages/web-ui/src/components/MessageList.ts
T
Mario Zechner 05dfaa11a8 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
2025-10-06 13:45:08 +02:00

93 lines
2.6 KiB
TypeScript

import { html } from "@mariozechner/mini-lit";
import type {
AgentTool,
AssistantMessage as AssistantMessageType,
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: AppMessage[] = [];
@property({ type: Array }) tools: AgentTool[] = [];
@property({ type: Object }) pendingToolCalls?: Set<string>;
@property({ type: Boolean }) isStreaming: boolean = false;
protected override createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
override connectedCallback(): void {
super.connectedCallback();
this.style.display = "block";
}
private buildRenderItems() {
// Map tool results by call id for quick lookup
const resultByCallId = new Map<string, ToolResultMessageType>();
for (const message of this.messages) {
if (message.role === "toolResult") {
resultByCallId.set(message.toolCallId, message);
}
}
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}`,
template: html`<user-message .message=${msg}></user-message>`,
});
index++;
} else if (msg.role === "assistant") {
const amsg = msg as AssistantMessageType;
items.push({
key: `msg:${index}`,
template: html`<assistant-message
.message=${amsg}
.tools=${this.tools}
.isStreaming=${this.isStreaming}
.pendingToolCalls=${this.pendingToolCalls}
.toolResultsById=${resultByCallId}
.hideToolCalls=${false}
></assistant-message>`,
});
index++;
} else {
// Skip standalone toolResult messages; they are rendered via paired tool-message above
// Skip unknown roles
}
}
return items;
}
override render() {
const items = this.buildRenderItems();
return html`<div class="flex flex-col gap-3">
${repeat(
items,
(it) => it.key,
(it) => it.template,
)}
</div>`;
}
}
// Register custom element
if (!customElements.get("message-list")) {
customElements.define("message-list", MessageList);
}