mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 02:43:22 +00:00
fix(app): use chronological message boundaries (#41006)
Co-authored-by: Dax <mail@thdxr.com>
This commit is contained in:
parent
9113255114
commit
5aa5cb3523
8 changed files with 37 additions and 23 deletions
|
|
@ -123,7 +123,8 @@ export function SessionContextTab() {
|
|||
() => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
|
|
|
|||
|
|
@ -1851,7 +1851,9 @@ export default function Page() {
|
|||
|
||||
const session = sdk().api.session
|
||||
const target = sync()
|
||||
const next = userMessages().find((item) => item.id > id)
|
||||
const index = userMessages().findIndex((item) => item.id === id)
|
||||
if (index < 0) return
|
||||
const next = userMessages()[index + 1]
|
||||
const last = target.session.get(sessionID)?.revert
|
||||
|
||||
await runPromptRollbackMutation({
|
||||
|
|
@ -1891,8 +1893,10 @@ export default function Page() {
|
|||
const rolled = createMemo(() => {
|
||||
const id = revertMessageID()
|
||||
if (!id) return []
|
||||
const index = userMessages().findIndex((item) => item.id === id)
|
||||
if (index < 0) return []
|
||||
return userMessages()
|
||||
.filter((item) => item.id >= id)
|
||||
.slice(index)
|
||||
.map((item) => ({ id: item.id, text: line(item.id) }))
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -287,7 +287,9 @@ export function MessageTimeline(props: {
|
|||
const visible = new Set(props.userMessages.map((message) => message.id))
|
||||
const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
|
||||
const messages = sync().data.session_message[id] ?? []
|
||||
return boundary ? messages.filter((message) => message.id < boundary) : messages
|
||||
if (!boundary) return messages
|
||||
const index = messages.findIndex((message) => message.id === boundary)
|
||||
return index < 0 ? messages : messages.slice(0, index)
|
||||
})
|
||||
const info = createMemo(() => {
|
||||
const id = sessionID()
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ const assistant = (id: string) => ({ id, role: "assistant" }) as AssistantMessag
|
|||
|
||||
describe("timeline model", () => {
|
||||
test("selects users and applies the revert boundary", () => {
|
||||
const messages: Message[] = [user("msg_1"), assistant("msg_2"), user("msg_3"), user("msg_5")]
|
||||
const messages: Message[] = [user("msg_z"), assistant("msg_a"), user("msg_b"), user("msg_c")]
|
||||
const users = selectUserMessages(messages)
|
||||
|
||||
expect(users.map((message) => message.id)).toEqual(["msg_1", "msg_3", "msg_5"])
|
||||
expect(selectVisibleUserMessages(users, "msg_5").map((message) => message.id)).toEqual(["msg_1", "msg_3"])
|
||||
expect(users.map((message) => message.id)).toEqual(["msg_z", "msg_b", "msg_c"])
|
||||
expect(selectVisibleUserMessages(users, "msg_b").map((message) => message.id)).toEqual(["msg_z"])
|
||||
expect(selectVisibleUserMessages(users)).toBe(users)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ export function isTimelineReady(messages: Message[] | undefined, loading: boolea
|
|||
|
||||
export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) {
|
||||
if (!revertMessageID) return messages
|
||||
return messages.filter((message) => message.id < revertMessageID)
|
||||
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
||||
return boundary < 0 ? messages : messages.slice(0, boundary)
|
||||
}
|
||||
|
||||
export async function loadOlderTimeline(input: {
|
||||
|
|
|
|||
|
|
@ -137,11 +137,11 @@ describe("current session timeline rows", () => {
|
|||
|
||||
test("renders an optimistic user turn and thinking before the protocol message arrives", () => {
|
||||
const source = [
|
||||
{ id: "msg_1", type: "user", text: "existing", time: { created: 1 } },
|
||||
{ id: "msg_z", type: "user", text: "existing", time: { created: 1 } },
|
||||
] satisfies SessionMessageInfo[]
|
||||
const normalized = normalizeSessionMessages("ses_1", source)
|
||||
const optimistic = {
|
||||
id: "msg_2",
|
||||
id: "msg_a",
|
||||
sessionID: "ses_1",
|
||||
role: "user" as const,
|
||||
time: { created: 2 },
|
||||
|
|
@ -161,10 +161,10 @@ describe("current session timeline rows", () => {
|
|||
|
||||
expect(result.activeMessageID).toBe(optimistic.id)
|
||||
expect(result.rows.map(TimelineRow.key)).toEqual([
|
||||
"user-message:msg_1",
|
||||
"turn-gap:msg_2",
|
||||
"user-message:msg_2",
|
||||
"thinking:msg_2",
|
||||
"user-message:msg_z",
|
||||
"turn-gap:msg_a",
|
||||
"user-message:msg_a",
|
||||
"thinking:msg_a",
|
||||
])
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai
|
|||
import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part"
|
||||
import { TimelineRow, type SummaryDiff } from "./timeline-row"
|
||||
import { uniqueSummaryDiffs } from "./summary-diffs"
|
||||
import { compareMessages } from "@/utils/session-message"
|
||||
|
||||
export { TimelineRow, type SummaryDiff } from "./timeline-row"
|
||||
|
||||
|
|
@ -71,12 +72,12 @@ export namespace Timeline {
|
|||
turns.push(turn)
|
||||
turnByUserID.set(user.id, turn)
|
||||
})
|
||||
const latestUserMessageID = turns.at(-1)?.user.id
|
||||
projectedUserMessages.forEach((user) => {
|
||||
if (turnByUserID.has(user.id)) return
|
||||
if (latestUserMessageID && user.id < latestUserMessageID) return
|
||||
const turn = { user, assistants: [] }
|
||||
turns.push(turn)
|
||||
const index = turns.findIndex((item) => compareMessages(user, item.user) < 0)
|
||||
if (index < 0) turns.push(turn)
|
||||
if (index >= 0) turns.splice(index, 0, turn)
|
||||
turnByUserID.set(user.id, turn)
|
||||
})
|
||||
const activeMessageID = turns.at(-1)?.user.id
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const visibleUserMessages = () => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||
}
|
||||
|
||||
const showAllFiles = () => {
|
||||
|
|
@ -337,7 +338,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const promptSession = prompt.capture()
|
||||
const revert = info()?.revert?.messageID
|
||||
const messages = userMessages()
|
||||
const message = findLast(messages, (x) => !revert || x.id < revert)
|
||||
const boundary = revert ? messages.findIndex((message) => message.id === revert) : messages.length
|
||||
if (boundary < 0) return
|
||||
const message = messages[boundary - 1]
|
||||
if (!message) return
|
||||
const parts = sync().data.part[message.id]
|
||||
|
||||
|
|
@ -352,7 +355,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
updatePrompt: (promptSession) => {
|
||||
if (parts) promptSession.set(extractPromptFromParts(parts, { directory }))
|
||||
},
|
||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < message.id)),
|
||||
updateViewport: () => setActiveMessage(messages[boundary - 2]),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -367,14 +370,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
|
||||
const next = messages.find((x) => x.id > revertMessageID)
|
||||
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
||||
if (boundary < 0) return
|
||||
const next = messages[boundary + 1]
|
||||
if (!next) {
|
||||
await runCommand({
|
||||
owner,
|
||||
prompt: promptSession,
|
||||
request: () => session.revert.clear({ sessionID }),
|
||||
updatePrompt: (promptSession) => promptSession.reset(),
|
||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)),
|
||||
updateViewport: () => setActiveMessage(messages.at(-1)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -384,7 +389,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
prompt: promptSession,
|
||||
request: () => session.revert.stage({ sessionID, messageID: next.id }),
|
||||
updatePrompt: () => undefined,
|
||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)),
|
||||
updateViewport: () => setActiveMessage(messages[boundary]),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue