finish migration to message-store

This commit is contained in:
Shantur Rathore
2025-11-26 10:13:05 +00:00
parent 93a5c16cab
commit 27cd4515cd
7 changed files with 111 additions and 169 deletions

View File

@@ -2,8 +2,8 @@ import { For, Show, createMemo, createSignal, createEffect, onCleanup } from "so
import MessageItem from "./message-item" import MessageItem from "./message-item"
import ToolCall from "./tool-call" import ToolCall from "./tool-call"
import Kbd from "./kbd" import Kbd from "./kbd"
import type { Message, MessageInfo, ClientPart } from "../types/message" import type { Message, MessageInfo, ClientPart, MessageDisplayParts } from "../types/message"
import { computeDisplayParts } from "../stores/session-messages" import { partHasRenderableText } from "../types/message"
import { getSessionInfo } from "../stores/sessions" import { getSessionInfo } from "../stores/sessions"
import { showCommandPalette } from "../stores/command-palette" import { showCommandPalette } from "../stores/command-palette"
import { messageStoreBus } from "../stores/message-v2/bus" import { messageStoreBus } from "../stores/message-v2/bus"
@@ -71,6 +71,27 @@ function recordToMessage(record: MessageRecord): Message {
} }
} }
function computeDisplayPartsForMessage(message: Message, showThinking: boolean): MessageDisplayParts {
const text: ClientPart[] = []
const tool: ClientPart[] = []
const reasoning: ClientPart[] = []
for (const part of message.parts) {
if (part.type === "text" && !part.synthetic && partHasRenderableText(part)) {
text.push(part)
} else if (part.type === "tool") {
tool.push(part)
} else if (part.type === "reasoning" && showThinking && partHasRenderableText(part)) {
reasoning.push(part)
}
}
const combined = reasoning.length > 0 ? [...text, ...reasoning] : [...text]
const version = typeof message.version === "number" ? message.version : 0
return { text, tool, reasoning, combined, showThinking, version }
}
function hasRenderableContent(message: Message, combinedParts: ClientPart[], info?: MessageInfo): boolean { function hasRenderableContent(message: Message, combinedParts: ClientPart[], info?: MessageInfo): boolean {
if (message.type !== "assistant" && message.type !== "user") { if (message.type !== "assistant" && message.type !== "user") {
return false return false
@@ -156,7 +177,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
} }
const baseMessage = recordToMessage(record) const baseMessage = recordToMessage(record)
const displayParts = computeDisplayParts(baseMessage, showThinking) const displayParts = computeDisplayPartsForMessage(baseMessage, showThinking)
baseMessage.displayParts = displayParts baseMessage.displayParts = displayParts
const combinedParts = displayParts.combined const combinedParts = displayParts.combined
const messageInfo = infoMap.get(record.id) const messageInfo = infoMap.get(record.id)

View File

@@ -258,7 +258,7 @@ export function useCommands(options: UseCommandsOptions) {
const revertState = store.getSessionRevert(sessionId) ?? session.revert const revertState = store.getSessionRevert(sessionId) ?? session.revert
let after = 0 let after = 0
if (revertState?.messageID) { if (revertState?.messageID) {
const revertInfo = infoMap.get(revertState.messageID) ?? session.messagesInfo.get(revertState.messageID) const revertInfo = infoMap.get(revertState.messageID) ?? store.getMessageInfo(revertState.messageID)
after = revertInfo?.time?.created || 0 after = revertInfo?.time?.created || 0
} }
@@ -267,7 +267,7 @@ export function useCommands(options: UseCommandsOptions) {
for (let i = messageIds.length - 1; i >= 0; i--) { for (let i = messageIds.length - 1; i >= 0; i--) {
const id = messageIds[i] const id = messageIds[i]
const record = store.getMessage(id) const record = store.getMessage(id)
const info = infoMap.get(id) const info = infoMap.get(id) ?? store.getMessageInfo(id)
if (record?.role === "user" && info?.time?.created) { if (record?.role === "user" && info?.time?.created) {
if (after > 0 && info.time.created >= after) { if (after > 0 && info.time.created >= after) {
continue continue
@@ -278,25 +278,6 @@ export function useCommands(options: UseCommandsOptions) {
} }
} }
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) { if (!messageID) {
showAlertDialog("Nothing to undo", { showAlertDialog("Nothing to undo", {
title: "No actions to undo", title: "No actions to undo",
@@ -312,16 +293,8 @@ export function useCommands(options: UseCommandsOptions) {
}) })
if (!restoredText) { if (!restoredText) {
const revertedMessage = session.messages.find((m) => m.id === messageID) const fallbackRecord = store.getMessage(messageID)
const revertedInfo = session.messagesInfo.get(messageID) restoredText = extractUserTextFromRecord(fallbackRecord)
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) { if (restoredText) {

View File

@@ -1,7 +1,8 @@
import { createSignal } from "solid-js" import { createSignal } from "solid-js"
import { produce } from "solid-js/store"
import type { Instance, LogEntry } from "../types/instance" import type { Instance, LogEntry } from "../types/instance"
import type { LspStatus, Permission } from "@opencode-ai/sdk" import type { LspStatus, Permission } from "@opencode-ai/sdk"
import type { ClientPart, Message } from "../types/message" import type { ClientPart } from "../types/message"
import { sdkManager } from "../lib/sdk-manager" import { sdkManager } from "../lib/sdk-manager"
import { sseManager } from "../lib/sse-manager" import { sseManager } from "../lib/sse-manager"
import { serverApi } from "../lib/api-client" import { serverApi } from "../lib/api-client"
@@ -17,9 +18,10 @@ import {
} from "./sessions" } from "./sessions"
import { fetchCommands, clearCommands } from "./commands" import { fetchCommands, clearCommands } from "./commands"
import { preferences } from "./preferences" import { preferences } from "./preferences"
import { computeDisplayParts } from "./session-messages" import { setSessionPendingPermission } from "./session-state"
import { withSession, setSessionPendingPermission } from "./session-state"
import { setHasInstances } from "./ui" import { setHasInstances } from "./ui"
import { messageStoreBus } from "./message-v2/bus"
import type { MessageRecord } from "./message-v2/types"
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map()) const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
@@ -539,23 +541,49 @@ function getPermissionSessionId(permission: Permission): string {
return (permission as any).sessionID return (permission as any).sessionID
} }
function findToolPartForPermission(message: Message, permission: Permission): ClientPart | null { function getPermissionMessageId(permission: Permission): string | undefined {
const expectedCallId = permission.callID return (permission as any).messageID ?? (permission as any).messageId ?? undefined
for (const part of message.parts) { }
if (part.type !== "tool") continue
const toolCallId = (part as any).callID function getPermissionCallIdentifier(permission: Permission): string | undefined {
return (
(permission as any).callID ??
(permission as any).callId ??
(permission as any).toolCallID ??
(permission as any).toolCallId ??
undefined
)
}
function findToolPartForPermission(record: MessageRecord, permission: Permission): { partId: string; part: ClientPart } | null {
const expectedCallId = getPermissionCallIdentifier(permission)
const permissionId = permission.id
const permissionMessageId = getPermissionMessageId(permission)
for (const partId of record.partIds) {
const entry = record.parts[partId]
if (!entry) continue
const part = entry.data
if (!part || part.type !== "tool") continue
const toolCallId = (part as any).callID ?? (part as any).callId
const partMessageId = (part as any).messageID ?? (part as any).messageId
if (expectedCallId) { if (expectedCallId) {
if (toolCallId === expectedCallId) { if (toolCallId === expectedCallId) {
return part as ClientPart return { partId, part }
} }
if (!toolCallId && (part.id === expectedCallId || part.messageID === permission.messageID)) { if (!toolCallId && (part.id === expectedCallId || (permissionMessageId && partMessageId === permissionMessageId))) {
return part as ClientPart return { partId, part }
} }
continue continue
} }
if ((toolCallId && toolCallId === permission.id) || part.id === permission.id || part.messageID === permission.messageID) { if (
return part as ClientPart (toolCallId && toolCallId === permissionId) ||
part.id === permissionId ||
(permissionMessageId && partMessageId === permissionMessageId)
) {
return { partId, part }
} }
} }
return null return null
@@ -564,23 +592,31 @@ function findToolPartForPermission(message: Message, permission: Permission): Cl
function mutateToolPartPermission( function mutateToolPartPermission(
instanceId: string, instanceId: string,
permission: Permission, permission: Permission,
mutator: (part: ClientPart, message: Message) => boolean, mutator: (part: ClientPart) => boolean,
): void { ): void {
const permissionSessionId = getPermissionSessionId(permission) const messageId = getPermissionMessageId(permission)
withSession(instanceId, permissionSessionId, (session) => { if (!messageId) return
const message = session.messages.find((msg) => msg.id === permission.messageID) const store = messageStoreBus.getOrCreate(instanceId)
if (!message) return const messageRecord = store.getMessage(messageId)
const targetPart = findToolPartForPermission(message, permission) if (!messageRecord) return
if (!targetPart) return const targetPart = findToolPartForPermission(messageRecord, permission)
if (!targetPart) return
const changed = mutator(targetPart, message) store.setState(
if (!changed) return "messages",
messageId,
const nextPartVersion = typeof targetPart.version === "number" ? targetPart.version + 1 : 1 produce((draft: MessageRecord) => {
targetPart.version = nextPartVersion const partRecord = draft.parts[targetPart.partId]
message.version = (message.version ?? 0) + 1 if (!partRecord) return
message.displayParts = computeDisplayParts(message, preferences().showThinkingBlocks) const changed = mutator(partRecord.data)
}) if (!changed) return
const nextVersion = typeof partRecord.data.version === "number" ? partRecord.data.version + 1 : 1
partRecord.data.version = nextVersion
partRecord.revision += 1
draft.revision += 1
draft.updatedAt = Date.now()
}),
)
} }
function attachPermissionToToolPart(instanceId: string, permission: Permission, active: boolean): void { function attachPermissionToToolPart(instanceId: string, permission: Permission, active: boolean): void {

View File

@@ -156,11 +156,12 @@ export function removePermissionV2(instanceId: string, permissionId: string): vo
export function ensureSessionMetadataV2(instanceId: string, session: Session | null | undefined): void { export function ensureSessionMetadataV2(instanceId: string, session: Session | null | undefined): void {
if (!session) return if (!session) return
const store = messageStoreBus.getOrCreate(instanceId) const store = messageStoreBus.getOrCreate(instanceId)
const existingMessageIds = store.getSessionMessageIds(session.id)
store.addOrUpdateSession({ store.addOrUpdateSession({
id: session.id, id: session.id,
title: session.title, title: session.title,
parentId: session.parentId ?? null, parentId: session.parentId ?? null,
messageIds: session.messages.map((message: Message) => message.id), messageIds: existingMessageIds,
}) })
} }

View File

@@ -2,7 +2,7 @@ import type { Session } from "../types/session"
import type { Message } from "../types/message" import type { Message } from "../types/message"
import { instances, refreshPermissionsForSession } from "./instances" import { instances, refreshPermissionsForSession } from "./instances"
import { preferences, setAgentModelPreference } from "./preferences" import { setAgentModelPreference } from "./preferences"
import { setSessionCompactionState } from "./session-compaction" import { setSessionCompactionState } from "./session-compaction"
import { import {
activeSessionId, activeSessionId,
@@ -22,16 +22,7 @@ import {
setLoading, setLoading,
} from "./session-state" } from "./session-state"
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models" import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
import { import { normalizeMessagePart, updateSessionInfo } from "./session-messages"
computeDisplayParts,
clearSessionIndex,
getSessionIndex,
initializePartVersion,
normalizeMessagePart,
rebuildSessionIndex,
rebuildSessionUsage,
updateSessionInfo,
} from "./session-messages"
import { seedSessionMessagesV2 } from "./message-v2/bridge" import { seedSessionMessagesV2 } from "./message-v2/bridge"
interface SessionForkResponse { interface SessionForkResponse {
@@ -101,8 +92,8 @@ async function fetchSessions(instanceId: string): Promise<void> {
diff: apiSession.revert.diff, diff: apiSession.revert.diff,
} }
: undefined, : undefined,
messages: existingSession?.messages ?? [], messages: [],
messagesInfo: existingSession?.messagesInfo ?? new Map(), messagesInfo: new Map(),
}) })
} }
@@ -238,8 +229,6 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
return next return next
}) })
getSessionIndex(instanceId, session.id)
return session return session
} catch (error) { } catch (error) {
console.error("Failed to create session:", error) console.error("Failed to create session:", error)
@@ -341,8 +330,6 @@ async function forkSession(
return next return next
}) })
getSessionIndex(instanceId, forkedSession.id)
return forkedSession return forkedSession
} }
@@ -391,8 +378,6 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
return next return next
}) })
clearSessionIndex(instanceId, sessionId)
if (activeSessionId().get(instanceId) === sessionId) { if (activeSessionId().get(instanceId) === sessionId) {
setActiveSessionId((prev) => { setActiveSessionId((prev) => {
const next = new Map(prev) const next = new Map(prev)
@@ -522,8 +507,8 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
}) })
try { try {
console.log(`[HTTP] GET /session.messages for instance ${instanceId}`, { sessionId }) console.log(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
const response = await instance.client.session.messages({ path: { id: sessionId } }) const response = await instance.client.session["messages"]({ path: { id: sessionId } })
if (!response.data || !Array.isArray(response.data)) { if (!response.data || !Array.isArray(response.data)) {
return return
@@ -549,10 +534,6 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
version: 0, version: 0,
} }
parts.forEach((part: any) => initializePartVersion(part))
message.displayParts = computeDisplayParts(message, preferences().showThinkingBlocks)
return message return message
}) })
@@ -587,8 +568,6 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
if (existingSession) { if (existingSession) {
const updatedSession = { const updatedSession = {
...existingSession, ...existingSession,
messages,
messagesInfo,
agent: agentName || existingSession.agent, agent: agentName || existingSession.agent,
model: providerID && modelID ? { providerId: providerID, modelId: modelID } : existingSession.model, model: providerID && modelID ? { providerId: providerID, modelId: modelID } : existingSession.model,
} }
@@ -600,9 +579,6 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
return next return next
}) })
rebuildSessionIndex(instanceId, sessionId, messages)
rebuildSessionUsage(instanceId, sessionId, messagesInfo)
setMessagesLoaded((prev) => { setMessagesLoaded((prev) => {
const next = new Map(prev) const next = new Map(prev)
const loadedSet = next.get(instanceId) || new Set() const loadedSet = next.get(instanceId) || new Set()

View File

@@ -109,8 +109,6 @@ function withSession(instanceId: string, sessionId: string, updater: (session: S
const updatedSession = { const updatedSession = {
...session, ...session,
messages: [...session.messages],
messagesInfo: new Map(session.messagesInfo),
} }
setSessions((prev) => { setSessions((prev) => {

View File

@@ -1,5 +1,5 @@
import type { Session, SessionStatus } from "../types/session" import type { Session, SessionStatus } from "../types/session"
import type { Message, MessageInfo } from "../types/message" import type { MessageInfo } from "../types/message"
import type { MessageRecord } from "./message-v2/types" import type { MessageRecord } from "./message-v2/types"
import { sessions } from "./sessions" import { sessions } from "./sessions"
import { isSessionCompactionActive } from "./session-compaction" import { isSessionCompactionActive } from "./session-compaction"
@@ -55,38 +55,6 @@ function getLastMessageFromStore(instanceId: string, sessionId: string): Message
return latest return latest
} }
function getLegacyLastMessage(session: Session): Message | undefined {
let latest: Message | undefined
let latestTimestamp = Number.NEGATIVE_INFINITY
for (const message of session.messages) {
if (!message) continue
const info = session.messagesInfo.get(message.id)
const timestamp = info?.time?.created ?? message.timestamp ?? Number.NEGATIVE_INFINITY
if (timestamp >= latestTimestamp) {
latest = message
latestTimestamp = timestamp
}
}
return latest
}
function getLegacyLastMessageInfo(session: Session, role?: MessageInfo["role"]): MessageInfo | undefined {
if (session.messagesInfo.size === 0) {
return undefined
}
let latest: MessageInfo | undefined
let latestTimestamp = Number.NEGATIVE_INFINITY
for (const info of session.messagesInfo.values()) {
if (!info) continue
if (role && info.role !== role) continue
const timestamp = info.time?.created ?? 0
if (timestamp >= latestTimestamp) {
latest = info
latestTimestamp = timestamp
}
}
return latest
}
function getInfoCreatedTimestamp(info?: MessageInfo): number { function getInfoCreatedTimestamp(info?: MessageInfo): number {
if (!info) { if (!info) {
@@ -143,26 +111,6 @@ function isAssistantStillGeneratingRecord(record: MessageRecord, info?: MessageI
return !(record.status === "complete" || record.status === "sent") return !(record.status === "complete" || record.status === "sent")
} }
function isAssistantStillGeneratingLegacy(message: Message, info?: MessageInfo): boolean {
if (message.type !== "assistant") {
return false
}
if (message.status === "error") {
return false
}
if (message.status === "streaming" || message.status === "sending") {
return true
}
const completedAt = (info?.time as { completed?: number } | undefined)?.completed
if (completedAt !== undefined && completedAt !== null) {
return false
}
return !(message.status === "complete" || message.status === "sent")
}
export function getSessionStatus(instanceId: string, sessionId: string): SessionStatus { export function getSessionStatus(instanceId: string, sessionId: string): SessionStatus {
const session = getSession(instanceId, sessionId) const session = getSession(instanceId, sessionId)
@@ -176,14 +124,13 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
return "compacting" return "compacting"
} }
const latestUserInfo = getLatestInfoFromStore(instanceId, sessionId, "user") ?? getLegacyLastMessageInfo(session, "user") const latestUserInfo = getLatestInfoFromStore(instanceId, sessionId, "user")
const latestAssistantInfo = getLatestInfoFromStore(instanceId, sessionId, "assistant") ?? getLegacyLastMessageInfo(session, "assistant") const latestAssistantInfo = getLatestInfoFromStore(instanceId, sessionId, "assistant")
const lastRecord = getLastMessageFromStore(instanceId, sessionId) const lastRecord = getLastMessageFromStore(instanceId, sessionId)
const legacyFallbackMessage = lastRecord ? undefined : getLegacyLastMessage(session)
if (!lastRecord && !legacyFallbackMessage) { if (!lastRecord) {
const latestInfo = latestUserInfo ?? latestAssistantInfo ?? getLegacyLastMessageInfo(session) const latestInfo = latestUserInfo ?? latestAssistantInfo
if (!latestInfo) { if (!latestInfo) {
return "idle" return "idle"
} }
@@ -194,22 +141,12 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
return infoCompleted ? "idle" : "working" return infoCompleted ? "idle" : "working"
} }
if (lastRecord) { if (lastRecord.role === "user") {
if (lastRecord.role === "user") { return "working"
return "working" }
} const infoForRecord = store.getMessageInfo(lastRecord.id) ?? latestAssistantInfo
const infoForRecord = store.getMessageInfo(lastRecord.id) ?? latestAssistantInfo if (infoForRecord && isAssistantStillGeneratingRecord(lastRecord, infoForRecord)) {
if (infoForRecord && isAssistantStillGeneratingRecord(lastRecord, infoForRecord)) { return "working"
return "working"
}
} else if (legacyFallbackMessage) {
if (legacyFallbackMessage.type === "user") {
return "working"
}
const infoForLegacy = session.messagesInfo.get(legacyFallbackMessage.id) ?? latestAssistantInfo
if (isAssistantStillGeneratingLegacy(legacyFallbackMessage, infoForLegacy)) {
return "working"
}
} }
if (isAssistantInfoPending(latestAssistantInfo)) { if (isAssistantInfoPending(latestAssistantInfo)) {