Skip to content

Commit 30a25e4

Browse files
committed
fix(app): user messages not rendering consistently
1 parent ea1aba4 commit 30a25e4

5 files changed

Lines changed: 61 additions & 29 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,7 +1463,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14631463
draft.part[messageID] = optimisticParts
14641464
.filter((p) => !!p?.id)
14651465
.slice()
1466-
.sort((a, b) => a.id.localeCompare(b.id))
1466+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
14671467
}),
14681468
)
14691469
return
@@ -1481,7 +1481,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14811481
draft.part[messageID] = optimisticParts
14821482
.filter((p) => !!p?.id)
14831483
.slice()
1484-
.sort((a, b) => a.id.localeCompare(b.id))
1484+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
14851485
}),
14861486
)
14871487
}

packages/app/src/context/global-sync.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ type ChildOptions = {
119119
bootstrap?: boolean
120120
}
121121

122+
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
123+
122124
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
123125
return {
124126
...input,
@@ -297,7 +299,7 @@ function createGlobalSync() {
297299
const aUpdated = sessionUpdatedAt(a)
298300
const bUpdated = sessionUpdatedAt(b)
299301
if (aUpdated !== bUpdated) return bUpdated - aUpdated
300-
return a.id.localeCompare(b.id)
302+
return cmp(a.id, b.id)
301303
}
302304

303305
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
@@ -325,7 +327,7 @@ function createGlobalSync() {
325327
const all = input
326328
.filter((s) => !!s?.id)
327329
.filter((s) => !s.time?.archived)
328-
.sort((a, b) => a.id.localeCompare(b.id))
330+
.sort((a, b) => cmp(a.id, b.id))
329331

330332
const roots = all.filter((s) => !s.parentID)
331333
const children = all.filter((s) => !!s.parentID)
@@ -342,7 +344,7 @@ function createGlobalSync() {
342344
return sessionUpdatedAt(s) > cutoff
343345
})
344346

345-
return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id))
347+
return [...keepRoots, ...keepChildren].sort((a, b) => cmp(a.id, b.id))
346348
}
347349

348350
function ensureChild(directory: string) {
@@ -457,7 +459,7 @@ function createGlobalSync() {
457459
const nonArchived = (x.data ?? [])
458460
.filter((s) => !!s?.id)
459461
.filter((s) => !s.time?.archived)
460-
.sort((a, b) => a.id.localeCompare(b.id))
462+
.sort((a, b) => cmp(a.id, b.id))
461463

462464
// Read the current limit at resolve-time so callers that bump the limit while
463465
// a request is in-flight still get the expanded result.
@@ -559,7 +561,7 @@ function createGlobalSync() {
559561
"permission",
560562
sessionID,
561563
reconcile(
562-
permissions.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)),
564+
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
563565
{ key: "id" },
564566
),
565567
)
@@ -588,7 +590,7 @@ function createGlobalSync() {
588590
"question",
589591
sessionID,
590592
reconcile(
591-
questions.filter((q) => !!q?.id).sort((a, b) => a.id.localeCompare(b.id)),
593+
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
592594
{ key: "id" },
593595
),
594596
)
@@ -986,7 +988,7 @@ function createGlobalSync() {
986988
.filter((p) => !!p?.id)
987989
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
988990
.slice()
989-
.sort((a, b) => a.id.localeCompare(b.id))
991+
.sort((a, b) => cmp(a.id, b.id))
990992
setGlobalStore("project", projects)
991993
}),
992994
),

packages/app/src/context/sync.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type { Message, Part } from "@opencode-ai/sdk/v2/client"
99

1010
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
1111

12+
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
13+
1214
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
1315
name: "Sync",
1416
init: () => {
@@ -59,7 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
5961
const next = items
6062
.map((x) => x.info)
6163
.filter((m) => !!m?.id)
62-
.sort((a, b) => a.id.localeCompare(b.id))
64+
.sort((a, b) => cmp(a.id, b.id))
6365

6466
batch(() => {
6567
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
@@ -69,7 +71,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
6971
"part",
7072
message.info.id,
7173
reconcile(
72-
message.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)),
74+
message.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
7375
{ key: "id" },
7476
),
7577
)
@@ -129,7 +131,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
129131
const result = Binary.search(messages, input.messageID, (m) => m.id)
130132
messages.splice(result.index, 0, message)
131133
}
132-
draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id))
134+
draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id))
133135
}),
134136
)
135137
},
@@ -271,7 +273,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
271273
await client.session.list().then((x) => {
272274
const sessions = (x.data ?? [])
273275
.filter((s) => !!s?.id)
274-
.sort((a, b) => a.id.localeCompare(b.id))
276+
.sort((a, b) => cmp(a.id, b.id))
275277
.slice(0, store.limit)
276278
setStore("session", reconcile(sessions, { key: "id" }))
277279
})

packages/app/src/pages/layout.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ export default function Layout(props: ParentProps) {
499499
const bUpdated = b.time.updated ?? b.time.created
500500
const aRecent = aUpdated > oneMinuteAgo
501501
const bRecent = bUpdated > oneMinuteAgo
502-
if (aRecent && bRecent) return a.id.localeCompare(b.id)
502+
if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
503503
if (aRecent && !bRecent) return -1
504504
if (!aRecent && bRecent) return 1
505505
return bUpdated - aUpdated
@@ -739,7 +739,7 @@ export default function Layout(props: ParentProps) {
739739
}
740740

741741
async function prefetchMessages(directory: string, sessionID: string, token: number) {
742-
const [, setStore] = globalSync.child(directory, { bootstrap: false })
742+
const [store, setStore] = globalSync.child(directory, { bootstrap: false })
743743

744744
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
745745
.then((messages) => {
@@ -750,23 +750,49 @@ export default function Layout(props: ParentProps) {
750750
.map((x) => x.info)
751751
.filter((m) => !!m?.id)
752752
.slice()
753-
.sort((a, b) => a.id.localeCompare(b.id))
753+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
754+
755+
const current = store.message[sessionID] ?? []
756+
const merged = (() => {
757+
if (current.length === 0) return next
758+
759+
const map = new Map<string, Message>()
760+
for (const item of current) {
761+
if (!item?.id) continue
762+
map.set(item.id, item)
763+
}
764+
for (const item of next) {
765+
map.set(item.id, item)
766+
}
767+
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
768+
})()
754769

755770
batch(() => {
756-
setStore("message", sessionID, reconcile(next, { key: "id" }))
771+
setStore("message", sessionID, reconcile(merged, { key: "id" }))
757772

758773
for (const message of items) {
759-
setStore(
760-
"part",
761-
message.info.id,
762-
reconcile(
763-
message.parts
774+
const currentParts = store.part[message.info.id] ?? []
775+
const mergedParts = (() => {
776+
if (currentParts.length === 0) {
777+
return message.parts
764778
.filter((p) => !!p?.id)
765779
.slice()
766-
.sort((a, b) => a.id.localeCompare(b.id)),
767-
{ key: "id" },
768-
),
769-
)
780+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
781+
}
782+
783+
const map = new Map<string, (typeof currentParts)[number]>()
784+
for (const item of currentParts) {
785+
if (!item?.id) continue
786+
map.set(item.id, item)
787+
}
788+
for (const item of message.parts) {
789+
if (!item?.id) continue
790+
map.set(item.id, item)
791+
}
792+
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
793+
})()
794+
795+
setStore("part", message.info.id, reconcile(mergedParts, { key: "id" }))
770796
}
771797
})
772798
})

packages/ui/src/components/session-turn.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,14 @@ export function SessionTurn(
161161
const messageIndex = createMemo(() => {
162162
const messages = allMessages() ?? emptyMessages
163163
const result = Binary.search(messages, props.messageID, (m) => m.id)
164-
if (!result.found) return -1
165164

166-
const msg = messages[result.index]
165+
const index = result.found ? result.index : messages.findIndex((m) => m.id === props.messageID)
166+
if (index < 0) return -1
167+
168+
const msg = messages[index]
167169
if (!msg || msg.role !== "user") return -1
168170

169-
return result.index
171+
return index
170172
})
171173

172174
const message = createMemo(() => {

0 commit comments

Comments
 (0)