Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSync } from "@/context/sync"
import { useLayout } from "@/context/layout"
import { checksum } from "@opencode-ai/util/encode"
import { findLast } from "@opencode-ai/util/array"
import { sortMessages } from "@opencode-ai/util/message"
import { same } from "@/utils/same"
import { Icon } from "@opencode-ai/ui/icon"
import { Accordion } from "@opencode-ai/ui/accordion"
Expand Down Expand Up @@ -104,7 +105,7 @@ export function SessionContextTab() {
() => {
const id = params.id
if (!id) return emptyMessages
return (sync.data.message[id] ?? []) as Message[]
return sortMessages((sync.data.message[id] ?? []) as Message[])
},
emptyMessages,
{ equals: same },
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { extractPromptFromParts } from "@/utils/prompt"
import { same } from "@/utils/same"
import { formatServerError } from "@/utils/server-errors"
import { sortMessages } from "@opencode-ai/util/message"

const emptyUserMessages: UserMessage[] = []

Expand Down
7 changes: 3 additions & 4 deletions packages/app/src/pages/session/message-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
import { showToast } from "@opencode-ai/ui/toast"
import { Binary } from "@opencode-ai/util/binary"
import { getFilename } from "@opencode-ai/util/path"
import { sortMessages } from "@opencode-ai/util/message"
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
import { SessionContextUsage } from "@/components/session-context-usage"
import { useDialog } from "@opencode-ai/ui/context/dialog"
Expand Down Expand Up @@ -227,7 +227,7 @@ export function MessageTimeline(props: {
const sessionMessages = createMemo(() => {
const id = sessionID()
if (!id) return emptyMessages
return sync.data.message[id] ?? emptyMessages
return sortMessages(sync.data.message[id] ?? emptyMessages)
})
const pending = createMemo(() =>
sessionMessages().findLast(
Expand Down Expand Up @@ -277,8 +277,7 @@ export function MessageTimeline(props: {
const parentID = pending()?.parentID
if (parentID) {
const messages = sessionMessages()
const result = Binary.search(messages, parentID, (message) => message.id)
const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID)
const message = messages.find((item) => item.id === parentID)
if (message && message.role === "user") return message.id
}

Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/pages/session/use-session-commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { DialogFork } from "@/components/dialog-fork"
import { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/util/array"
import { sortMessages } from "@opencode-ai/util/message"
import { extractPromptFromParts } from "@/utils/prompt"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { canAddSelectionContext } from "@/pages/session/session-command-helpers"
Expand Down Expand Up @@ -56,7 +57,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {

const idle = { type: "idle" as const }
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const messages = createMemo(() => (params.id ? sortMessages(sync.data.message[params.id] ?? []) : []))
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[])
const visibleUserMessages = createMemo(() => {
const revert = info()?.revert?.messageID
Expand Down
60 changes: 17 additions & 43 deletions packages/ui/src/components/session-turn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { SessionStatus } from "@opencode-ai/sdk/v2"
import { useData } from "../context"
import { useFileComponent } from "../context/file"

import { Binary } from "@opencode-ai/util/binary"
import { selectAssistants, sortMessages } from "@opencode-ai/util/message"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { Dynamic } from "solid-js/web"
Expand Down Expand Up @@ -166,29 +166,15 @@ export function SessionTurn(
const emptyDiffs: FileDiff[] = []
const idle = { type: "idle" as const }

const allMessages = createMemo(() => list(data.store.message?.[props.sessionID], emptyMessages))

const messageIndex = createMemo(() => {
const messages = allMessages() ?? emptyMessages
const result = Binary.search(messages, props.messageID, (m) => m.id)

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 index
})
const allMessages = createMemo(
() => sortMessages(list(data.store.message?.[props.sessionID], emptyMessages)),
emptyMessages,
{ equals: same },
)

const message = createMemo(() => {
const index = messageIndex()
if (index < 0) return undefined

const messages = allMessages() ?? emptyMessages
const msg = messages[index]
const msg = allMessages().find((message) => message.id === props.messageID && message.role === "user")
if (!msg || msg.role !== "user") return undefined

return msg
})

Expand All @@ -203,9 +189,7 @@ export function SessionTurn(
const pendingUser = createMemo(() => {
const item = pending()
if (!item?.parentID) return
const messages = allMessages() ?? emptyMessages
const result = Binary.search(messages, item.parentID, (m) => m.id)
const msg = result.found ? messages[result.index] : messages.find((m) => m.id === item.parentID)
const msg = allMessages().find((message) => message.id === item.parentID)
if (!msg || msg.role !== "user") return
return msg
})
Expand All @@ -220,12 +204,14 @@ export function SessionTurn(

const queued = createMemo(() => {
if (typeof props.queued === "boolean") return props.queued
const id = message()?.id
if (!id) return false
if (!pendingUser()) return false
const item = pending()
if (!item) return false
return id > item.id
const msg = message()
const parent = pendingUser()
if (!msg || !parent) return false
const users = allMessages().filter((item) => item.role === "user")
const current = users.findIndex((item) => item.id === msg.id)
const pending = users.findIndex((item) => item.id === parent.id)
if (current === -1 || pending === -1) return false
return current > pending
})

const parts = createMemo(() => {
Expand Down Expand Up @@ -268,19 +254,7 @@ export function SessionTurn(
() => {
const msg = message()
if (!msg) return emptyAssistant

const messages = allMessages() ?? emptyMessages
const index = messageIndex()
if (index < 0) return emptyAssistant

const result: AssistantMessage[] = []
for (let i = index + 1; i < messages.length; i++) {
const item = messages[i]
if (!item) continue
if (item.role === "user") break
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
}
return result
return selectAssistants(allMessages(), msg.id) as AssistantMessage[]
},
emptyAssistant,
{ equals: same },
Expand Down
39 changes: 39 additions & 0 deletions packages/util/src/message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test"
import { selectAssistants, sortMessages, splitMessages } from "./message"

describe("message", () => {
test("sortMessages uses created time before id", () => {
const result = sortMessages([
{ id: "msg_z", role: "assistant", time: { created: 20 } },
{ id: "msg_a", role: "user", time: { created: 10 } },
])

expect(result.map((item) => item.id)).toEqual(["msg_a", "msg_z"])
})

test("selectAssistants finds replies even when assistant id sorts before user id", () => {
const result = selectAssistants(
[
{ id: "msg_user", role: "user", time: { created: 10 } },
{ id: "msg_assistant", role: "assistant", parentID: "msg_user", time: { created: 11 } },
],
"msg_user",
)

expect(result.map((item) => item.id)).toEqual(["msg_assistant"])
})

test("splitMessages uses chronological order instead of id order", () => {
const result = splitMessages(
[
{ id: "msg_3", role: "user", time: { created: 30 } },
{ id: "msg_1", role: "user", time: { created: 10 } },
{ id: "msg_2", role: "user", time: { created: 20 } },
],
"msg_2",
)

expect(result.before.map((item) => item.id)).toEqual(["msg_1"])
expect(result.after.map((item) => item.id)).toEqual(["msg_2", "msg_3"])
})
})
44 changes: 44 additions & 0 deletions packages/util/src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
type Message = {
id: string
role?: string
parentID?: string
time?: {
created?: number
}
}

function rank(message: Message) {
if (message.role === "user") return 0
if (message.role === "assistant") return 1
return 2
}

export function compareMessages(a: Message, b: Message) {
const at = a.time?.created ?? 0
const bt = b.time?.created ?? 0
if (at !== bt) return at - bt

const ar = rank(a)
const br = rank(b)
if (ar !== br) return ar - br

if (a.id < b.id) return -1
if (a.id > b.id) return 1
return 0
}

export function sortMessages<T extends Message>(messages: readonly T[]) {
return messages.slice().sort(compareMessages)
}

export function selectAssistants<T extends Message>(messages: readonly T[], parentID: string) {
return sortMessages(messages.filter((message) => message.role === "assistant" && message.parentID === parentID))
}

export function splitMessages<T extends Message>(messages: readonly T[], markerID?: string) {
const sorted = sortMessages(messages)
if (!markerID) return { before: sorted, after: [] as T[] }
const index = sorted.findIndex((message) => message.id === markerID)
if (index === -1) return { before: sorted, after: [] as T[] }
return { before: sorted.slice(0, index), after: sorted.slice(index) }
}
Loading