import { For, Show, createMemo, createSignal, createEffect, on, onMount } from "solid-js"; import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"; import { useEi } from "../context/ei.js"; import { useKeyboardNav } from "../context/keyboard.js"; import { logger } from "../util/logger.js"; import { solarizedDarkSyntax } from "../util/syntax.js"; import type { Quote, Message } from "../../../src/core/types.js"; import { insertQuoteMarkers } from "../util/quote-utils.js"; interface MessageWithQuotes extends Message { _quotes: Quote[]; _quoteIndex: number; } function formatTime(timestamp: string): string { const date = new Date(timestamp); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); return `${hours}:${minutes}`; } function getContent(msg: { content?: string }): string { return msg.content ?? ''; } function buildMessageText(message: Message, humanName: string): string { if (message.silence_reason !== undefined) { const silentParty = message.role === "human" ? humanName : "Persona"; return `[${silentParty} chose not to respond: ${message.silence_reason}]`; } return getContent(message); } let instanceId = 0; export function MessageList() { const myId = ++instanceId; logger.info(`MessageList instance ${myId} MOUNTED`); const { messages, activePersonaId, personas, activeContextBoundary, getQuotes, quotesVersion, getHuman } = useEi(); const { focusedPanel, registerMessageScroll } = useKeyboardNav(); const isFocused = () => focusedPanel() === "messages"; const [humanDisplayName, setHumanDisplayName] = createSignal("Human"); const [allQuotes, setAllQuotes] = createSignal([]); onMount(() => { void getHuman().then(human => { const name = human.settings?.name_display || human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description || "Human"; setHumanDisplayName(name); }); }); createEffect(on(() => [messages(), quotesVersion()], () => { void getQuotes().then(setAllQuotes); })); const messagesWithQuotes = createMemo(() => { const quotesByMessage = new Map(); for (const quote of allQuotes()) { if (quote.message_id) { const existing = quotesByMessage.get(quote.message_id) ?? []; existing.push(quote); quotesByMessage.set(quote.message_id, existing); } } return messages().map((msg, idx) => { const quotes = quotesByMessage.get(msg.id) ?? []; return { ...msg, _quotes: quotes, _quoteIndex: idx + 1, }; }); }); const handleScrollboxRef = (scrollbox: ScrollBoxRenderable) => { registerMessageScroll(scrollbox); }; const boundaryIsActive = createMemo(() => { const boundary = activeContextBoundary(); const msgs = messages(); const lastMessage = msgs[msgs.length - 1]; return boundary ? (!lastMessage || boundary > lastMessage.timestamp) : false; }); return ( 0} fallback={ } > {(message, index) => { const getDisplayName = () => { const persona = personas().find(p => p.id === activePersonaId()); return persona?.display_name ?? "Ei"; }; const speaker = message.role === "human" ? humanDisplayName() : getDisplayName(); const speakerColor = message.role === "human" ? "#2aa198" : "#b58900"; const header = () => `${speaker} (${formatTime(message.timestamp)}) [✂️ ${message._quoteIndex}]:`; const displayContent = insertQuoteMarkers(buildMessageText(message, humanDisplayName()), message._quotes); const showDivider = () => { const boundary = activeContextBoundary(); if (!boundary) return false; const i = index(); if (i === 0) return false; const msgs = messagesWithQuotes(); const prevMsg = msgs[i - 1]; const result = prevMsg.timestamp < boundary && message.timestamp >= boundary; if (result) { logger.debug(`showDivider TRUE at index ${i}: prev=${prevMsg.timestamp}, boundary=${boundary}, curr=${message.timestamp}`); } return result; }; const showTrailingDivider = () => { const msgs = messagesWithQuotes(); const isLast = index() === msgs.length - 1; if (!isLast) return false; return boundaryIsActive(); }; return ( <> ); }} ); }