From 63d70529c4d08a508332200c74d0c8685da73ce1 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Thu, 7 May 2026 14:15:18 +0200 Subject: [PATCH] Extract chat grouping model --- src/client/src/chatGroups.ts | 47 +++++++++++++++++++++++++++ src/client/src/components/ChatView.ts | 47 ++------------------------- 2 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 src/client/src/chatGroups.ts diff --git a/src/client/src/chatGroups.ts b/src/client/src/chatGroups.ts new file mode 100644 index 0000000..6d1abe8 --- /dev/null +++ b/src/client/src/chatGroups.ts @@ -0,0 +1,47 @@ +import type { ChatLine, ChatPart } from "./components/shared"; + +export type ChatGroup = + | { kind: "message"; message: ChatLine; index: number } + | { kind: "group"; messages: ChatLine[]; startIndex: number }; + +export function groupChatMessages(messages: ChatLine[]): ChatGroup[] { + const groups: ChatGroup[] = []; + let eventMessages: ChatLine[] = []; + let eventStartIndex = 0; + + const pushEvent = (message: ChatLine, index: number) => { + if (!eventMessages.length) eventStartIndex = index; + eventMessages.push(message); + }; + const flushEvents = () => { + if (!eventMessages.length) return; + groups.push({ kind: "group", messages: eventMessages, startIndex: eventStartIndex }); + eventMessages = []; + }; + + messages.forEach((message, index) => { + const readableParts = message.parts.filter((part) => isReadablePart(message, part)); + const technicalParts = message.parts.filter((part) => !isReadablePart(message, part)); + + if (technicalParts.length) pushEvent({ role: message.role, parts: technicalParts }, index); + if (readableParts.length) { + flushEvents(); + groups.push({ kind: "message", message: { role: message.role, parts: readableParts }, index }); + } + }); + flushEvents(); + return groups; +} + +export function summarizeChatGroup(messages: ChatLine[]): string { + const counts = messages.reduce>((acc, message) => { + acc[message.role] = (acc[message.role] ?? 0) + 1; + return acc; + }, {}); + const details = Object.entries(counts).map(([role, count]) => `${count} ${role}`).join(" · "); + return `${messages.length} ${messages.length === 1 ? "event" : "events"}${details ? ` · ${details}` : ""}`; +} + +function isReadablePart(message: ChatLine, part: ChatPart): boolean { + return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "bash"); +} diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index 1379d72..a3943e3 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -1,5 +1,6 @@ import { LitElement, html } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; +import { groupChatMessages, summarizeChatGroup } from "../chatGroups"; import type { ChatLine, ChatPart } from "./shared"; import { chatStyles } from "./shared"; import "./FormattedText"; @@ -30,7 +31,7 @@ export class ChatView extends LitElement { render() { return html`
- ${this.groupedMessages().map((group) => group.kind === "message" + ${groupChatMessages(this.messages).map((group) => group.kind === "message" ? this.renderMessage(group.message, group.index) : this.renderMessageGroup(group.messages, group.startIndex))}
@@ -51,7 +52,7 @@ export class ChatView extends LitElement {
events - ${this.groupSummary(messages)} + ${summarizeChatGroup(messages)}
${messages.map((message) => html` @@ -65,48 +66,6 @@ export class ChatView extends LitElement { `; } - private groupedMessages(): Array<{ kind: "message"; message: ChatLine; index: number } | { kind: "group"; messages: ChatLine[]; startIndex: number }> { - const groups: Array<{ kind: "message"; message: ChatLine; index: number } | { kind: "group"; messages: ChatLine[]; startIndex: number }> = []; - let eventMessages: ChatLine[] = []; - let eventStartIndex = 0; - - const pushEvent = (message: ChatLine, index: number) => { - if (!eventMessages.length) eventStartIndex = index; - eventMessages.push(message); - }; - const flushEvents = () => { - if (!eventMessages.length) return; - groups.push({ kind: "group", messages: eventMessages, startIndex: eventStartIndex }); - eventMessages = []; - }; - - this.messages.forEach((message, index) => { - const readableParts = message.parts.filter((part) => this.isReadablePart(message, part)); - const technicalParts = message.parts.filter((part) => !this.isReadablePart(message, part)); - - if (technicalParts.length) pushEvent({ role: message.role, parts: technicalParts }, index); - if (readableParts.length) { - flushEvents(); - groups.push({ kind: "message", message: { role: message.role, parts: readableParts }, index }); - } - }); - flushEvents(); - return groups; - } - - private isReadablePart(message: ChatLine, part: ChatPart): boolean { - return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "bash"); - } - - private groupSummary(messages: ChatLine[]): string { - const counts = messages.reduce>((acc, message) => { - acc[message.role] = (acc[message.role] ?? 0) + 1; - return acc; - }, {}); - const details = Object.entries(counts).map(([role, count]) => `${count} ${role}`).join(" · "); - return `${messages.length} ${messages.length === 1 ? "event" : "events"}${details ? ` · ${details}` : ""}`; - } - private renderPart(part: ChatPart) { if (part.type === "text") return html``; if (part.type === "thinking") return html`
thinking
`;