Skip to content

TUI /redo can move the revert boundary backwards and delete session history #43034

Description

@as4193

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:

  1. /undo — correct, the last turn is marked reverted (boundary = msg_aaaa…B).
  2. /redo — expected: revert cleared. Actual: the boundary jumps to msg_zzzz…A, the first message, so the whole conversation shows as reverted.
  3. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions