Description
The TUI redo command still picks its target by comparing message ID strings:
// packages/tui/src/routes/session/index.tsx (line 659 on dev @ 4d68d30b)
const message = messages().find((x) => x.role === "user" && x.id > messageID)
messages() is ordered by time.created (that's what #40994 fixed), but this one lookup still assumes ID order == chronological order. When a session's message IDs are not in chronological order, find can return a message that sits before the revert boundary. Redo then stages the revert at that earlier message, i.e. it marks more history as reverted instead of less.
That state is not just cosmetic: the next normal turn commits the revert, and every message from the boundary onward is removed from the session. One /undo, one /redo, one follow-up message can wipe a whole conversation.
The sibling code paths were all converted to positional lookups in #40994 (/undo uses messagesBeforeRevert().findLast(...), revertRevertedMessages uses findIndex + slice) and the web client's redo in packages/app/src/pages/session/use-session-commands.tsx already uses findIndex + messages[boundary + 1]. The TUI redo looks like a missed spot in that conversion.
PR follows.
Plugins
none
OpenCode version
1.18.15 – 1.18.18, still present on dev at 4d68d30
Steps to reproduce
You need a session whose message IDs do not sort in chronological order. The message ID is a caller-supplied field on the prompt endpoint (id, optional,^msg_), so this is reproducible without touching the database:
# any provider works
opencode serve --port 4096 &
SES=$(curl -s -X POST http://127.0.0.1:4096/api/session \
-H 'content-type: application/json' -d '{}' | jq -r .data.id)
# first turn gets the HIGH id, second turn the LOW id
curl -s -X POST "http://127.0.0.1:4096/api/session/$SES/prompt" \
-H 'content-type: application/json' \
-d '{"id":"msg_zzzzzzzzzzzz0000000000000A","prompt":{"text":"say one"}}'
# wait for the turn to finish, then:
curl -s -X POST "http://127.0.0.1:4096/api/session/$SES/prompt" \
-H 'content-type: application/json' \
-d '{"id":"msg_aaaaaaaaaaaa0000000000000B","prompt":{"text":"say two"}}'
Then open that session in the TUI:
/undo — correct, the last turn is marked reverted (boundary = msg_aaaa…B).
/redo — expected: revert cleared. Actual: the boundary jumps to msg_zzzz…A, the first message, so the whole conversation shows as reverted.
- Send any message — the revert is committed and the reverted messages are gone for good.
Same thing happens on sessions that were imported or migrated from elsewhere, where IDs were minted in a different order than the timestamps they carry.
Minimal check of just the selection, no server needed:
const messages = [
{ id: "msg_zzz001", role: "user" }, // older, higher id
{ id: "msg_zzz002", role: "assistant" },
{ id: "msg_aaa001", role: "user" }, // newer, lower id <- revert boundary
{ id: "msg_aaa002", role: "assistant" },
]
messages.find((x) => x.role === "user" && x.id > "msg_aaa001")
// -> msg_zzz001, which is at index 0, before the boundary
Screenshot and/or share link
No response
Operating System
macOS 15.5
Terminal
iTerm2
Description
The TUI redo command still picks its target by comparing message ID strings:
messages()is ordered bytime.created(that's what #40994 fixed), but this one lookup still assumes ID order == chronological order. When a session's message IDs are not in chronological order,findcan return a message that sits before the revert boundary. Redo then stages the revert at that earlier message, i.e. it marks more history as reverted instead of less.That state is not just cosmetic: the next normal turn commits the revert, and every message from the boundary onward is removed from the session. One
/undo, one/redo, one follow-up message can wipe a whole conversation.The sibling code paths were all converted to positional lookups in #40994 (
/undousesmessagesBeforeRevert().findLast(...),revertRevertedMessagesusesfindIndex+slice) and the web client's redo inpackages/app/src/pages/session/use-session-commands.tsxalready usesfindIndex+messages[boundary + 1]. The TUI redo looks like a missed spot in that conversion.PR follows.
Plugins
none
OpenCode version
1.18.15 – 1.18.18, still present on dev at 4d68d30
Steps to reproduce
You need a session whose message IDs do not sort in chronological order. The message ID is a caller-supplied field on the prompt endpoint (
id, optional,^msg_), so this is reproducible without touching the database:Then open that session in the TUI:
/undo— correct, the last turn is marked reverted (boundary =msg_aaaa…B)./redo— expected: revert cleared. Actual: the boundary jumps tomsg_zzzz…A, the first message, so the whole conversation shows as reverted.Same thing happens on sessions that were imported or migrated from elsewhere, where IDs were minted in a different order than the timestamps they carry.
Minimal check of just the selection, no server needed:
Screenshot and/or share link
No response
Operating System
macOS 15.5
Terminal
iTerm2