chore: add message store v2 baseline

This commit is contained in:
Shantur Rathore
2025-11-26 09:42:10 +00:00
parent 9313b2bd6c
commit 16b76385e2
13 changed files with 1665 additions and 57 deletions

View File

@@ -0,0 +1,97 @@
interface CacheLocation {
instanceId?: string
sessionId?: string
scope?: string
}
const GLOBAL_KEY = "GLOBAL"
type CacheScope = Map<string, unknown>
type ScopeCollection = Map<string, CacheScope>
type SessionMap = Map<string, ScopeCollection>
const cacheRoot = new Map<string, SessionMap>()
function resolveKey(value?: string) {
return value && value.length > 0 ? value : GLOBAL_KEY
}
function resolveCacheScope(location: CacheLocation, createIfMissing: boolean): CacheScope | undefined {
const instanceKey = resolveKey(location.instanceId)
const sessionKey = resolveKey(location.sessionId)
const scopeKey = resolveKey(location.scope)
let sessionMap = cacheRoot.get(instanceKey)
if (!sessionMap) {
if (!createIfMissing) return undefined
sessionMap = new Map()
cacheRoot.set(instanceKey, sessionMap)
}
let scopeCollection = sessionMap.get(sessionKey)
if (!scopeCollection) {
if (!createIfMissing) return undefined
scopeCollection = new Map()
sessionMap.set(sessionKey, scopeCollection)
}
let cacheScope = scopeCollection.get(scopeKey)
if (!cacheScope) {
if (!createIfMissing) return undefined
cacheScope = new Map()
scopeCollection.set(scopeKey, cacheScope)
}
return cacheScope
}
export function setGlobalCacheValue(location: CacheLocation, key: string, value: unknown): void {
const cacheScope = resolveCacheScope(location, true)
cacheScope?.set(key, value)
}
export function getGlobalCacheValue<T = unknown>(location: CacheLocation, key: string): T | undefined {
const cacheScope = resolveCacheScope(location, false)
return (cacheScope?.get(key) as T | undefined) ?? undefined
}
export function deleteGlobalCacheValue(location: CacheLocation, key: string): void {
const cacheScope = resolveCacheScope(location, false)
cacheScope?.delete(key)
}
export function clearGlobalCacheScope(location: CacheLocation): void {
const instanceKey = resolveKey(location.instanceId)
const sessionKey = resolveKey(location.sessionId)
const scopeKey = resolveKey(location.scope)
const sessionMap = cacheRoot.get(instanceKey)
if (!sessionMap) return
const scopeCollection = sessionMap.get(sessionKey)
if (!scopeCollection) return
scopeCollection.delete(scopeKey)
if (scopeCollection.size === 0) {
sessionMap.delete(sessionKey)
}
if (sessionMap.size === 0) {
cacheRoot.delete(instanceKey)
}
}
export function clearGlobalCacheSession(instanceId?: string, sessionId?: string): void {
const instanceKey = resolveKey(instanceId)
const sessionKey = resolveKey(sessionId)
const sessionMap = cacheRoot.get(instanceKey)
if (!sessionMap) return
sessionMap.delete(sessionKey)
if (sessionMap.size === 0) {
cacheRoot.delete(instanceKey)
}
}
export function clearGlobalCacheInstance(instanceId?: string): void {
const instanceKey = resolveKey(instanceId)
cacheRoot.delete(instanceKey)
}
export function clearAllGlobalCache(): void {
cacheRoot.clear()
}

View File

@@ -3,6 +3,7 @@ import type { Accessor } from "solid-js"
import type { Preferences, ExpansionPreference } from "../../stores/preferences"
import { createCommandRegistry, type Command } from "../commands"
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
import type { ClientPart, MessageInfo } from "../../types/message"
import {
activeParentSessionId,
activeSessionId as activeSessionMap,
@@ -13,6 +14,8 @@ import {
import { setSessionCompactionState } from "../../stores/session-compaction"
import { showAlertDialog } from "../../stores/alerts"
import type { Instance } from "../../types/instance"
import type { MessageRecord } from "../../stores/message-v2/types"
import { messageStoreBus } from "../../stores/message-v2/bus"
export interface UseCommandsOptions {
preferences: Accessor<Preferences>
@@ -29,6 +32,18 @@ export interface UseCommandsOptions {
getActiveSessionIdForInstance: () => string | null
}
function extractUserTextFromRecord(record?: MessageRecord): string | null {
if (!record) return null
const parts = record.partIds
.map((partId) => record.parts[partId]?.data)
.filter((part): part is ClientPart => Boolean(part))
const textParts = parts.filter((part): part is ClientPart & { type: "text"; text: string } => part.type === "text" && typeof (part as any).text === "string")
if (textParts.length === 0) {
return null
}
return textParts.map((part) => (part as any).text as string).join("\n")
}
export function useCommands(options: UseCommandsOptions) {
const commandRegistry = createCommandRegistry()
const [commands, setCommands] = createSignal<Command[]>([])
@@ -232,34 +247,56 @@ export function useCommands(options: UseCommandsOptions) {
const session = sessions.find((s) => s.id === sessionId)
if (!session) return
let after = 0
const revert = session.revert
const store = messageStoreBus.getOrCreate(instance.id)
const messageIds = store.getSessionMessageIds(sessionId)
const infoMap = new Map<string, MessageInfo>()
messageIds.forEach((id) => {
const info = store.getMessageInfo(id)
if (info) infoMap.set(id, info)
})
if (revert?.messageID) {
for (let i = session.messages.length - 1; i >= 0; i--) {
const msg = session.messages[i]
const info = session.messagesInfo.get(msg.id)
if (info?.id === revert.messageID) {
after = info.time?.created || 0
break
}
}
const revertState = store.getSessionRevert(sessionId) ?? session.revert
let after = 0
if (revertState?.messageID) {
const revertInfo = infoMap.get(revertState.messageID) ?? session.messagesInfo.get(revertState.messageID)
after = revertInfo?.time?.created || 0
}
let messageID = ""
for (let i = session.messages.length - 1; i >= 0; i--) {
const msg = session.messages[i]
const info = session.messagesInfo.get(msg.id)
if (msg.type === "user" && info?.time?.created) {
let restoredText: string | null = null
for (let i = messageIds.length - 1; i >= 0; i--) {
const id = messageIds[i]
const record = store.getMessage(id)
const info = infoMap.get(id)
if (record?.role === "user" && info?.time?.created) {
if (after > 0 && info.time.created >= after) {
continue
}
messageID = msg.id
messageID = id
restoredText = extractUserTextFromRecord(record)
break
}
}
if (!messageID) {
for (let i = session.messages.length - 1; i >= 0; i--) {
const msg = session.messages[i]
const info = session.messagesInfo.get(msg.id)
if (msg.type === "user" && info?.time?.created) {
if (after > 0 && info.time.created >= after) {
continue
}
messageID = msg.id
const textParts = msg.parts.filter((p): p is ClientPart & { type: "text"; text: string } => p.type === "text" && typeof (p as any).text === "string")
if (textParts.length > 0) {
restoredText = textParts.map((p) => (p as any).text as string).join("\n")
}
break
}
}
}
if (!messageID) {
showAlertDialog("Nothing to undo", {
title: "No actions to undo",
@@ -274,20 +311,27 @@ export function useCommands(options: UseCommandsOptions) {
body: { messageID },
})
const revertedMessage = session.messages.find((m) => m.id === messageID)
const revertedInfo = session.messagesInfo.get(messageID)
if (revertedMessage && revertedInfo?.role === "user") {
const textParts = revertedMessage.parts.filter((p) => p.type === "text")
if (textParts.length > 0) {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
if (textarea) {
textarea.value = textParts.map((p: any) => p.text).join("\n")
textarea.dispatchEvent(new Event("input", { bubbles: true }))
textarea.focus()
if (!restoredText) {
const revertedMessage = session.messages.find((m) => m.id === messageID)
const revertedInfo = session.messagesInfo.get(messageID)
if (revertedMessage && revertedInfo?.role === "user") {
const textParts = revertedMessage.parts.filter(
(p): p is ClientPart & { type: "text"; text: string } => p.type === "text" && typeof (p as any).text === "string",
)
if (textParts.length > 0) {
restoredText = textParts.map((p) => (p as any).text as string).join("\n")
}
}
}
if (restoredText) {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
if (textarea) {
textarea.value = restoredText
textarea.dispatchEvent(new Event("input", { bubbles: true }))
textarea.focus()
}
}
} catch (error) {
console.error("Failed to revert message:", error)
showAlertDialog("Failed to revert message", {

View File

@@ -0,0 +1,53 @@
import type { ScrollSnapshot } from "../stores/message-v2/types"
interface ScrollCacheParams {
instanceId?: string
sessionId?: string
scope?: string
}
const scrollCache = new Map<string, ScrollSnapshot>()
const DEFAULT_SCOPE = "session"
function resolve(value?: string) {
return value && value.length > 0 ? value : "GLOBAL"
}
function makeKey(params: ScrollCacheParams) {
return `${resolve(params.instanceId)}:${resolve(params.sessionId)}:${params.scope ?? DEFAULT_SCOPE}`
}
export function setScrollCache(params: ScrollCacheParams, snapshot: Omit<ScrollSnapshot, "updatedAt">) {
scrollCache.set(makeKey(params), { ...snapshot, updatedAt: Date.now() })
}
export function getScrollCache(params: ScrollCacheParams): ScrollSnapshot | undefined {
return scrollCache.get(makeKey(params))
}
export function clearScrollCacheScope(params: ScrollCacheParams) {
const key = makeKey(params)
scrollCache.delete(key)
}
export function clearScrollCacheForSession(instanceId?: string, sessionId?: string) {
const match = `${resolve(instanceId)}:${resolve(sessionId)}:`
for (const key of scrollCache.keys()) {
if (key.startsWith(match)) {
scrollCache.delete(key)
}
}
}
export function clearScrollCacheForInstance(instanceId?: string) {
const match = `${resolve(instanceId)}:`
for (const key of scrollCache.keys()) {
if (key.startsWith(match)) {
scrollCache.delete(key)
}
}
}
export function clearAllScrollCache() {
scrollCache.clear()
}