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
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
}),
)
return
@@ -1481,7 +1481,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
draft.part[messageID] = optimisticParts
.filter((p) => !!p?.id)
.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
}
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
return {
...input,
@@ -297,7 +299,7 @@ function createGlobalSync() {
const aUpdated = sessionUpdatedAt(a)
const bUpdated = sessionUpdatedAt(b)
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) {
@@ -325,7 +327,7 @@ function createGlobalSync() {
const all = input
.filter((s) => !!s?.id)
.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 children = all.filter((s) => !!s.parentID)
@@ -342,7 +344,7 @@ function createGlobalSync() {
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) {
@@ -457,7 +459,7 @@ function createGlobalSync() {
const nonArchived = (x.data ?? [])
.filter((s) => !!s?.id)
.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
// a request is in-flight still get the expanded result.
@@ -559,7 +561,7 @@ function createGlobalSync() {
"permission",
sessionID,
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" },
),
)
@@ -588,7 +590,7 @@ function createGlobalSync() {
"question",
sessionID,
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" },
),
)
@@ -986,7 +988,7 @@ function createGlobalSync() {
.filter((p) => !!p?.id)
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.sort((a, b) => cmp(a.id, b.id))
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 cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
@@ -59,7 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const next = items
.map((x) => x.info)
.filter((m) => !!m?.id)
.sort((a, b) => a.id.localeCompare(b.id))
.sort((a, b) => cmp(a.id, b.id))
batch(() => {
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
@@ -69,7 +71,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
"part",
message.info.id,
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" },
),
)
@@ -129,7 +131,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const result = Binary.search(messages, input.messageID, (m) => m.id)
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) => {
const sessions = (x.data ?? [])
.filter((s) => !!s?.id)
.sort((a, b) => a.id.localeCompare(b.id))
.sort((a, b) => cmp(a.id, b.id))
.slice(0, store.limit)
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 aRecent = aUpdated > 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
return bUpdated - aUpdated
@@ -739,7 +739,7 @@ export default function Layout(props: ParentProps) {
}
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 }))
.then((messages) => {
@@ -750,23 +750,49 @@ export default function Layout(props: ParentProps) {
.map((x) => x.info)
.filter((m) => !!m?.id)
.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(() => {
setStore("message", sessionID, reconcile(next, { key: "id" }))
setStore("message", sessionID, reconcile(merged, { key: "id" }))
for (const message of items) {
setStore(
"part",
message.info.id,
reconcile(
message.parts
const currentParts = store.part[message.info.id] ?? []
const mergedParts = (() => {
if (currentParts.length === 0) {
return message.parts
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
}
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 messages = allMessages() ?? emptyMessages
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
return result.index
return index
})
const message = createMemo(() => {