Browse Source

fix(app): user messages not rendering consistently

Adam 3 weeks ago
parent
commit
30a25e4edc

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

@@ -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))
         }),
       )
     }

+ 9 - 7
packages/app/src/context/global-sync.tsx

@@ -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)
         }),
       ),

+ 6 - 4
packages/app/src/context/sync.tsx

@@ -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" }))
           })

+ 39 - 13
packages/app/src/pages/layout.tsx

@@ -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" }))
           }
         })
       })

+ 5 - 3
packages/ui/src/components/session-turn.tsx

@@ -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(() => {