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 ToolCall from "./tool-call"
import Kbd from "./kbd"
import type { Message, MessageInfo, ClientPart } from "../types/message"
import { computeDisplayParts } from "../stores/session-messages"
import type { Message, MessageInfo, ClientPart, MessageDisplayParts } from "../types/message"
import { partHasRenderableText } from "../types/message"
import { getSessionInfo } from "../stores/sessions"
import { showCommandPalette } from "../stores/command-palette"
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 {
if (message.type !== "assistant" && message.type !== "user") {
return false
@@ -156,7 +177,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
}
const baseMessage = recordToMessage(record)
const displayParts = computeDisplayParts(baseMessage, showThinking)
const displayParts = computeDisplayPartsForMessage(baseMessage, showThinking)
baseMessage.displayParts = displayParts
const combinedParts = displayParts.combined
const messageInfo = infoMap.get(record.id)

View File

@@ -258,7 +258,7 @@ export function useCommands(options: UseCommandsOptions) {
const revertState = store.getSessionRevert(sessionId) ?? session.revert
let after = 0
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
}
@@ -267,7 +267,7 @@ export function useCommands(options: UseCommandsOptions) {
for (let i = messageIds.length - 1; i >= 0; i--) {
const id = messageIds[i]
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 (after > 0 && info.time.created >= after) {
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) {
showAlertDialog("Nothing to undo", {
title: "No actions to undo",
@@ -312,16 +293,8 @@ export function useCommands(options: UseCommandsOptions) {
})
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")
}
}
const fallbackRecord = store.getMessage(messageID)
restoredText = extractUserTextFromRecord(fallbackRecord)
}
if (restoredText) {

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 { sessions } from "./sessions"
import { isSessionCompactionActive } from "./session-compaction"
@@ -55,38 +55,6 @@ function getLastMessageFromStore(instanceId: string, sessionId: string): Message
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 {
if (!info) {
@@ -143,26 +111,6 @@ function isAssistantStillGeneratingRecord(record: MessageRecord, info?: MessageI
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 {
const session = getSession(instanceId, sessionId)
@@ -176,14 +124,13 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
return "compacting"
}
const latestUserInfo = getLatestInfoFromStore(instanceId, sessionId, "user") ?? getLegacyLastMessageInfo(session, "user")
const latestAssistantInfo = getLatestInfoFromStore(instanceId, sessionId, "assistant") ?? getLegacyLastMessageInfo(session, "assistant")
const latestUserInfo = getLatestInfoFromStore(instanceId, sessionId, "user")
const latestAssistantInfo = getLatestInfoFromStore(instanceId, sessionId, "assistant")
const lastRecord = getLastMessageFromStore(instanceId, sessionId)
const legacyFallbackMessage = lastRecord ? undefined : getLegacyLastMessage(session)
if (!lastRecord && !legacyFallbackMessage) {
const latestInfo = latestUserInfo ?? latestAssistantInfo ?? getLegacyLastMessageInfo(session)
if (!lastRecord) {
const latestInfo = latestUserInfo ?? latestAssistantInfo
if (!latestInfo) {
return "idle"
}
@@ -194,22 +141,12 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
return infoCompleted ? "idle" : "working"
}
if (lastRecord) {
if (lastRecord.role === "user") {
return "working"
}
const infoForRecord = store.getMessageInfo(lastRecord.id) ?? latestAssistantInfo
if (infoForRecord && isAssistantStillGeneratingRecord(lastRecord, infoForRecord)) {
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 (lastRecord.role === "user") {
return "working"
}
const infoForRecord = store.getMessageInfo(lastRecord.id) ?? latestAssistantInfo
if (infoForRecord && isAssistantStillGeneratingRecord(lastRecord, infoForRecord)) {
return "working"
}
if (isAssistantInfoPending(latestAssistantInfo)) {