diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 74f6362f..a2a83f7e 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -875,7 +875,6 @@ const InstanceShell2: Component = (props) => {
lastAssistantIdx) - const info = messageInfo() - const infoTime = (info?.time ?? {}) as { created?: number; updated?: number; completed?: number } - const infoTimestamp = - typeof infoTime.completed === "number" - ? infoTime.completed - : typeof infoTime.updated === "number" - ? infoTime.updated - : infoTime.created ?? 0 - const infoError = (info as { error?: { name?: string } } | undefined)?.error - const infoErrorName = typeof infoError?.name === "string" ? infoError.name : "" + + // Intentionally untracked: messageInfoVersion updates should not trigger + // a full message block rebuild; record revision is the invalidation key. + const info = untrack(messageInfo) + const cacheSignature = [ current.id, current.revision, @@ -252,8 +247,6 @@ export default function MessageBlock(props: MessageBlockProps) { props.showThinking() ? 1 : 0, props.thinkingDefaultExpanded() ? 1 : 0, props.showUsageMetrics() ? 1 : 0, - infoTimestamp, - infoErrorName, ].join("|") const cachedBlock = sessionCache.messageBlocks.get(current.id) diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index e30388cc..31750e4b 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -1,5 +1,5 @@ import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js" -import type { Session, SessionStatus } from "../types/session" +import type { SessionStatus } from "../types/session" import type { SessionThread } from "../stores/session-state" import { getSessionStatus } from "../stores/session-status" import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid" @@ -14,6 +14,7 @@ import { isSessionParentExpanded, loading, renameSession, + sessions as sessionStateSessions, setActiveSessionFromList, toggleSessionParentExpanded, } from "../stores/sessions" @@ -25,7 +26,6 @@ const log = getLogger("session") interface SessionListProps { instanceId: string - sessions: Map threads: SessionThread[] activeSessionId: string | null onSelect: (sessionId: string) => void @@ -58,7 +58,7 @@ const SessionList: Component = (props) => { const selectSession = (sessionId: string) => { - const session = props.sessions.get(sessionId) + const session = sessionStateSessions().get(props.instanceId)?.get(sessionId) const parentId = session?.parentId ?? session?.id if (parentId) { ensureSessionParentExpanded(props.instanceId, parentId) @@ -132,7 +132,7 @@ const SessionList: Component = (props) => { } const openRenameDialog = (sessionId: string) => { - const session = props.sessions.get(sessionId) + const session = sessionStateSessions().get(props.instanceId)?.get(sessionId) if (!session) return const label = session.title && session.title.trim() ? session.title : sessionId setRenameTarget({ id: sessionId, title: session.title ?? "", label }) @@ -167,7 +167,7 @@ const SessionList: Component = (props) => { expanded?: boolean onToggleExpand?: () => void }> = (rowProps) => { - const session = () => props.sessions.get(rowProps.sessionId) + const session = createMemo(() => sessionStateSessions().get(props.instanceId)?.get(rowProps.sessionId)) if (!session()) { return <> } @@ -293,7 +293,7 @@ const SessionList: Component = (props) => { const activeId = props.activeSessionId if (!activeId || activeId === "info") return null - const activeSession = props.sessions.get(activeId) + const activeSession = sessionStateSessions().get(props.instanceId)?.get(activeId) if (!activeSession) return null return activeSession.parentId ?? activeSession.id diff --git a/packages/ui/src/stores/session-events.ts b/packages/ui/src/stores/session-events.ts index ab658e7a..13bd3e29 100644 --- a/packages/ui/src/stores/session-events.ts +++ b/packages/ui/src/stores/session-events.ts @@ -240,8 +240,20 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes const messageId = typeof info.id === "string" ? info.id : undefined if (!sessionId || !messageId) return + const timeInfo = (info.time ?? {}) as { created?: number; updated?: number; completed?: number } + const nextUpdated = + typeof timeInfo.completed === "number" && timeInfo.completed > 0 + ? timeInfo.completed + : typeof timeInfo.updated === "number" && timeInfo.updated > 0 + ? timeInfo.updated + : typeof timeInfo.created === "number" && timeInfo.created > 0 + ? timeInfo.created + : Date.now() + withSession(instanceId, sessionId, (session) => { - session.time = { ...(session.time ?? {}), updated: Date.now() } + const currentUpdated = session.time?.updated ?? 0 + if (nextUpdated <= currentUpdated) return false + session.time = { ...(session.time ?? {}), updated: nextUpdated } }) const store = messageStoreBus.getOrCreate(instanceId) diff --git a/packages/ui/src/stores/session-state.ts b/packages/ui/src/stores/session-state.ts index fbf2a982..87dfed9b 100644 --- a/packages/ui/src/stores/session-state.ts +++ b/packages/ui/src/stores/session-state.ts @@ -390,9 +390,35 @@ function getSessionFamily(instanceId: string, parentId: string): Session[] { return [parent, ...children] } +type SessionThreadCacheEntry = { + signature: string + thread: SessionThread +} + +type SessionThreadCache = { + byParentId: Map +} + +const sessionThreadCache = new Map() + +function getOrCreateSessionThreadCache(instanceId: string): SessionThreadCache { + let cache = sessionThreadCache.get(instanceId) + if (!cache) { + cache = { byParentId: new Map() } + sessionThreadCache.set(instanceId, cache) + } + return cache +} + function getSessionThreads(instanceId: string): SessionThread[] { const instanceSessions = sessions().get(instanceId) - if (!instanceSessions || instanceSessions.size === 0) return [] + if (!instanceSessions || instanceSessions.size === 0) { + sessionThreadCache.delete(instanceId) + return [] + } + + const cache = getOrCreateSessionThreadCache(instanceId) + const seenParents = new Set() const parents: Session[] = [] const childrenByParent = new Map() @@ -416,6 +442,8 @@ function getSessionThreads(instanceId: string): SessionThread[] { const threads: SessionThread[] = [] for (const parent of parents) { + seenParents.add(parent.id) + const children = childrenByParent.get(parent.id) ?? [] if (children.length > 1) { children.sort((a, b) => (b.time.updated ?? 0) - (a.time.updated ?? 0)) @@ -425,7 +453,23 @@ function getSessionThreads(instanceId: string): SessionThread[] { const latestChild = children[0]?.time.updated ?? 0 const latestUpdated = Math.max(parentUpdated, latestChild) - threads.push({ parent, children, latestUpdated }) + const childIds = children.map((child) => child.id).join(",") + const signature = `${parentUpdated}:${latestChild}:${childIds}` + + const cached = cache.byParentId.get(parent.id) + if (cached && cached.signature === signature) { + threads.push(cached.thread) + } else { + const thread: SessionThread = { parent, children, latestUpdated } + cache.byParentId.set(parent.id, { signature, thread }) + threads.push(thread) + } + } + + for (const parentId of Array.from(cache.byParentId.keys())) { + if (!seenParents.has(parentId)) { + cache.byParentId.delete(parentId) + } } threads.sort((a, b) => {