fix(app): user messages not rendering consistently

This commit is contained in:
Adam
2026-02-02 09:21:08 -06:00
parent ea1aba4192
commit 30a25e4edc
5 changed files with 61 additions and 29 deletions

View File

@@ -1463,7 +1463,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
draft.part[messageID] = optimisticParts draft.part[messageID] = optimisticParts
.filter((p) => !!p?.id) .filter((p) => !!p?.id)
.slice() .slice()
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
}), }),
) )
return return
@@ -1481,7 +1481,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
draft.part[messageID] = optimisticParts draft.part[messageID] = optimisticParts
.filter((p) => !!p?.id) .filter((p) => !!p?.id)
.slice() .slice()
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
}), }),
) )
} }

View File

@@ -119,6 +119,8 @@ type ChildOptions = {
bootstrap?: boolean bootstrap?: boolean
} }
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
return { return {
...input, ...input,
@@ -297,7 +299,7 @@ function createGlobalSync() {
const aUpdated = sessionUpdatedAt(a) const aUpdated = sessionUpdatedAt(a)
const bUpdated = sessionUpdatedAt(b) const bUpdated = sessionUpdatedAt(b)
if (aUpdated !== bUpdated) return bUpdated - aUpdated if (aUpdated !== bUpdated) return bUpdated - aUpdated
return a.id.localeCompare(b.id) return cmp(a.id, b.id)
} }
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) { function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
@@ -325,7 +327,7 @@ function createGlobalSync() {
const all = input const all = input
.filter((s) => !!s?.id) .filter((s) => !!s?.id)
.filter((s) => !s.time?.archived) .filter((s) => !s.time?.archived)
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => cmp(a.id, b.id))
const roots = all.filter((s) => !s.parentID) const roots = all.filter((s) => !s.parentID)
const children = all.filter((s) => !!s.parentID) const children = all.filter((s) => !!s.parentID)
@@ -342,7 +344,7 @@ function createGlobalSync() {
return sessionUpdatedAt(s) > cutoff return sessionUpdatedAt(s) > cutoff
}) })
return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id)) return [...keepRoots, ...keepChildren].sort((a, b) => cmp(a.id, b.id))
} }
function ensureChild(directory: string) { function ensureChild(directory: string) {
@@ -457,7 +459,7 @@ function createGlobalSync() {
const nonArchived = (x.data ?? []) const nonArchived = (x.data ?? [])
.filter((s) => !!s?.id) .filter((s) => !!s?.id)
.filter((s) => !s.time?.archived) .filter((s) => !s.time?.archived)
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => cmp(a.id, b.id))
// Read the current limit at resolve-time so callers that bump the limit while // Read the current limit at resolve-time so callers that bump the limit while
// a request is in-flight still get the expanded result. // a request is in-flight still get the expanded result.
@@ -559,7 +561,7 @@ function createGlobalSync() {
"permission", "permission",
sessionID, sessionID,
reconcile( reconcile(
permissions.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)), permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" }, { key: "id" },
), ),
) )
@@ -588,7 +590,7 @@ function createGlobalSync() {
"question", "question",
sessionID, sessionID,
reconcile( reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => a.id.localeCompare(b.id)), questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" }, { key: "id" },
), ),
) )
@@ -986,7 +988,7 @@ function createGlobalSync() {
.filter((p) => !!p?.id) .filter((p) => !!p?.id)
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
.slice() .slice()
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => cmp(a.id, b.id))
setGlobalStore("project", projects) setGlobalStore("project", projects)
}), }),
), ),

View File

@@ -9,6 +9,8 @@ import type { Message, Part } from "@opencode-ai/sdk/v2/client"
const keyFor = (directory: string, id: string) => `${directory}\n${id}` const keyFor = (directory: string, id: string) => `${directory}\n${id}`
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync", name: "Sync",
init: () => { init: () => {
@@ -59,7 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const next = items const next = items
.map((x) => x.info) .map((x) => x.info)
.filter((m) => !!m?.id) .filter((m) => !!m?.id)
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => cmp(a.id, b.id))
batch(() => { batch(() => {
input.setStore("message", input.sessionID, reconcile(next, { key: "id" })) input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
@@ -69,7 +71,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
"part", "part",
message.info.id, message.info.id,
reconcile( reconcile(
message.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)), message.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" }, { key: "id" },
), ),
) )
@@ -129,7 +131,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const result = Binary.search(messages, input.messageID, (m) => m.id) const result = Binary.search(messages, input.messageID, (m) => m.id)
messages.splice(result.index, 0, message) messages.splice(result.index, 0, message)
} }
draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)) draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id))
}), }),
) )
}, },
@@ -271,7 +273,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
await client.session.list().then((x) => { await client.session.list().then((x) => {
const sessions = (x.data ?? []) const sessions = (x.data ?? [])
.filter((s) => !!s?.id) .filter((s) => !!s?.id)
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => cmp(a.id, b.id))
.slice(0, store.limit) .slice(0, store.limit)
setStore("session", reconcile(sessions, { key: "id" })) setStore("session", reconcile(sessions, { key: "id" }))
}) })

View File

@@ -499,7 +499,7 @@ export default function Layout(props: ParentProps) {
const bUpdated = b.time.updated ?? b.time.created const bUpdated = b.time.updated ?? b.time.created
const aRecent = aUpdated > oneMinuteAgo const aRecent = aUpdated > oneMinuteAgo
const bRecent = bUpdated > oneMinuteAgo const bRecent = bUpdated > oneMinuteAgo
if (aRecent && bRecent) return a.id.localeCompare(b.id) if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
if (aRecent && !bRecent) return -1 if (aRecent && !bRecent) return -1
if (!aRecent && bRecent) return 1 if (!aRecent && bRecent) return 1
return bUpdated - aUpdated return bUpdated - aUpdated
@@ -739,7 +739,7 @@ export default function Layout(props: ParentProps) {
} }
async function prefetchMessages(directory: string, sessionID: string, token: number) { async function prefetchMessages(directory: string, sessionID: string, token: number) {
const [, setStore] = globalSync.child(directory, { bootstrap: false }) const [store, setStore] = globalSync.child(directory, { bootstrap: false })
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
.then((messages) => { .then((messages) => {
@@ -750,23 +750,49 @@ export default function Layout(props: ParentProps) {
.map((x) => x.info) .map((x) => x.info)
.filter((m) => !!m?.id) .filter((m) => !!m?.id)
.slice() .slice()
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
const current = store.message[sessionID] ?? []
const merged = (() => {
if (current.length === 0) return next
const map = new Map<string, Message>()
for (const item of current) {
if (!item?.id) continue
map.set(item.id, item)
}
for (const item of next) {
map.set(item.id, item)
}
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
})()
batch(() => { batch(() => {
setStore("message", sessionID, reconcile(next, { key: "id" })) setStore("message", sessionID, reconcile(merged, { key: "id" }))
for (const message of items) { for (const message of items) {
setStore( const currentParts = store.part[message.info.id] ?? []
"part", const mergedParts = (() => {
message.info.id, if (currentParts.length === 0) {
reconcile( return message.parts
message.parts
.filter((p) => !!p?.id) .filter((p) => !!p?.id)
.slice() .slice()
.sort((a, b) => a.id.localeCompare(b.id)), .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
{ key: "id" }, }
),
) const map = new Map<string, (typeof currentParts)[number]>()
for (const item of currentParts) {
if (!item?.id) continue
map.set(item.id, item)
}
for (const item of message.parts) {
if (!item?.id) continue
map.set(item.id, item)
}
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
})()
setStore("part", message.info.id, reconcile(mergedParts, { key: "id" }))
} }
}) })
}) })

View File

@@ -161,12 +161,14 @@ export function SessionTurn(
const messageIndex = createMemo(() => { const messageIndex = createMemo(() => {
const messages = allMessages() ?? emptyMessages const messages = allMessages() ?? emptyMessages
const result = Binary.search(messages, props.messageID, (m) => m.id) const result = Binary.search(messages, props.messageID, (m) => m.id)
if (!result.found) return -1
const msg = messages[result.index] const index = result.found ? result.index : messages.findIndex((m) => m.id === props.messageID)
if (index < 0) return -1
const msg = messages[index]
if (!msg || msg.role !== "user") return -1 if (!msg || msg.role !== "user") return -1
return result.index return index
}) })
const message = createMemo(() => { const message = createMemo(() => {