chore: add message store v2 baseline
This commit is contained in:
97
packages/ui/src/lib/global-cache.ts
Normal file
97
packages/ui/src/lib/global-cache.ts
Normal 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()
|
||||
}
|
||||
@@ -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", {
|
||||
|
||||
53
packages/ui/src/lib/scroll-cache.ts
Normal file
53
packages/ui/src/lib/scroll-cache.ts
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user